Fable full-portfolio review: XIS M365 / Azure Assessment Tool
Repository: X-Centric-IT-Solutions/prj-xis-m365_azure_assesor
Reviewed main: c728ea48e71674e8e4b052b7b16d5933a32a080f (pristine detached checkout under evidence/)
Review date: 2026-09-28
Reviewer: Fable, fresh context, read-only. Every code claim below is bound to the main SHA above unless a candidate SHA is named. Every candidate claim is bound to the SHA named beside it. "Observed" means I read it in the evidence; "inferred" is marked as such.
1. Executive summary
State of the project. The committed code on main is substantially better than the board suggests. The security core is real: per-customer SQLCipher databases under envelope-wrapped keys, a fail-closed safeStorage secret store, a typed IPC contract with per-handler validation and ownership assertions, encrypted-only .xisnap export, fail-closed report-file ownership for retention, and a SQLCipher-v4 adapter that verifies its cipher profile and key before handing out a connection. The CIS 287-control baseline is on main (PR #261), derived from the licensed workbooks by a hash-pinned extractor and drift-tested. The WAF denominator is pinned (59 items, manifest 2026.09.2). The build, typecheck, test, lint and audit jobs are green at the reviewed SHA. The one red CI job is the CIS independent human-approval gate, and that framing holds: it fails because no approval evidence and no licensed workbook secret are supplied, not because of a code defect.
What blocks v1. Five things, in order of leverage:
- Nothing has been run as a packaged app, and no collection has run against a live tenant. Every document says so. The native packaging matrix on main runs only on manual dispatch and has not been dispatched since it was rewritten. Until an installer exists, #109, #110 and #237 cannot start.
- The retention commitment is not reachable from the product. The 14-day purge exists in the main process and is tested, but no renderer code calls
projects:close-engagement,projects:record-sign-offorprojects:reopen-engagement. A consultant cannot close an engagement, soclosed_atis never set and the sweep never has a candidate.DATA-HANDLING.md§6.2 tells customers the tool enforces this. It does not, today. This is #118, open and unstarted. - Two unquarantined Azure rules and two WAF rules still violate the project's own "collection failure is never a verdict" rule. The Key Vault evaluators pass vacuously when the vault listing failed or no subscription was collected. Two WAF evaluators report a permissions failure as
fail. These are exactly the false-pass and false-negative classes the repo has documented three times. - Deliverables carry no provenance (#166). The shipped pdfkit report prints project, customer, snapshot label and date; it does not print benchmark versions, the rule-set hash, or the app version. The new HTML report template (#207) has landed but nothing renders it;
export:pdfstill uses pdfkit. - The CIS approval gate keeps main red by design, which means a real regression on main would look identical to the expected state. Either supply the approval evidence or move the gate off the push path.
What you can start today. Dispatch the native package matrix on main (it targets GitHub-hosted Windows and macOS runners; the "no Windows host in the fleet" blocker on the board does not apply to it). Rule on the two owner-decision cards using the dispositions in §8; four of the seven candidates they hold are already on main by content and should be retired as shipped. Open two bounded issues: the fail-closed evaluator fix and the #118 renderer wiring. Fix worktree provisioning before dispatching any more automated writers; a third of the fleet's worktrees are checked out with modes that manufacture false test failures.
Board and branches. Of 323 blocked cards, roughly 10 are genuine blockers. The rest are GitHub-issue mirrors, multi-round plan/review loop residue, and a fleet-tooling program. Of 83 branches ahead of main, at least 18 are exact heads of merged PRs and about 40 more are superseded intermediates; roughly 10 hold work worth keeping. The canonical working checkout is not lost work: its dirty tree is, by every measurable signal, main's content overlaid on a stale branch.
2. Verified facts and what could not be verified
2.1 Verified (observed in evidence)
| Fact | Value | Source |
|---|---|---|
| main SHA | c728ea48e71674e8e4b052b7b16d5933a32a080f | inputs/manifest.json, evidence/main-log-40.txt |
| main commit count | 217 | manifest |
| Last merge | PR #391, merged 2026-09-28T03:24:02Z | evidence/prs-closed-all.json |
| Open / closed issues | 186 / 81 | manifest, issues-compact.txt |
| Open / closed PRs | 0 / 124 | manifest |
| Kanban | 323 blocked, 37 todo, 1 ready, 1 triage, 481 done, 54 archived | manifest, kanban-active-compact.json; 481 done confirmed by grep |
| Branches ahead of main | 83 | wip-branch-census.json |
| Worktrees | 316 | worktree-list.txt |
| CI at main | 10 jobs: 6 success, 1 failure (CIS independent approval gate), 3 skipped (native-package-matrix, native-package, native-package-evidence: dispatch-only) | ci-main-jobs.json |
| Failing step | CIS independent approval gate (blocks until human approval), exit 1: two licensed workbooks absent, CIS_HUMAN_APPROVALS absent, three pending subjects (cis-azure-2.2.1, cis-azure-2.2.2, sourceDerived) | ci-main-failure-log.txt lines 419-426 |
| Same job's other steps | committed-bytes authority check ok, adversarial suite 47 tests OK | failure log lines 290-380 |
| Main CI conclusion history | every push run on main in the last 40 runs is failure (2026-09-22 through 09-28) | actions-runs-40.json |
| Last successful installer build | Windows Build (unsigned installer) dispatch at 89f23a22, 2026-09-23; and at 106ad99c, 2026-09-21. Both predate the current thin-front-door workflow | actions-runs-40.json |
| Live processes at launch | lane controller (controller.py run), one planner one-shot (hermes chat --profile coordinator, 43 s old), one coordinator shell doing git fetch in the m365-assessor-issue-171 worktree, Hermes infrastructure. Zero implementation or review workers | live-process-census.txt |
| Rule inventory | 173 cis-m365 + 136 cis-azure + 12 waf = 321 rules | grep on resources/rules/*.json |
| Rule-file versions | cis-m365 7.0.0, cis-azure 6.0.0, waf 2024 | rule file headers |
| Coverage matrix totals | toolAutomated 2, manualWorkflow 278, toolingGap 7 (of 287) | resources/cis-coverage-matrix.json lines 5-29 |
| Quarantined rules | 13: 11 cis-azure + waf-se-04 + waf-re-01 | cis-quarantine.test.ts lines 32-46, rule JSON |
Non-manual fn evaluators referenced by rules | 35 (14 m365, 13 azure, 8 waf) + 3 JMESPath (cis-azure Defender pricing, all quarantined) | grep counts |
| Vitest cases | 1794 it(/test( across 91 files | grep count |
| Driver | better-sqlite3-multiple-ciphers 13.0.3, prebuilds pinned by SHA-256 for win32-x64, darwin-x64, darwin-arm64, linux-x64 | package.json, scripts/native-package-policy.json |
@journeyapps/sqlcipher | absent from root package.json; present only in scripts/sqlcipher-legacy-harness/package.json (nested, its own lockfile, own CI job) | grep |
2.2 Could not be verified (read-only, no execution)
- Whether
npm testpasses on a fresh clone without Python andopenpyxl. CI installsopenpyxlbeforenpm testbecausesrc/main/rules/cis-authority.test.tsspawns the Python adversarial suite.docs/BUILD-AND-TEST.mdlists only Node as a prerequisite. - Whether the native package inspector produces
passfor any of the 11requiredCheckIdson real targets. The workflow has not been dispatched since the WP6 seam landed. The inspector header (scripts/inspect-native-package.mjslines 8-15) says capability-dependent checks reportblockeduntil WP3/WP4/WP5 adapters exist; WP3 and WP4 have since merged, WP5 has not. sandbox: truebehaviour on macOS and Windows. Never observed, perdocs/SIGNING.mdline 48 anddocs/BUILD-AND-TEST.mdline 125.- The canonical checkout at
/root/prj-xis-m365_azure_assesor: file reads were denied. The verdict in §7.3 is inferred from the coordinator's status and diffstat only. - The lane controller source (
controller.pyline 583) cited by the operations card: read denied. The card's claim stands unverified. - Content equality between candidate SHAs and main for the retire-as-shipped recommendations in §6. I verified by reading main for the named features, not by diffing the candidates.
3. Committed-code review findings (main @ c728ea48)
Severity per the owner's scale. "Class" separates code defect from missing evidence from deliberate gate.
Critical
F1. Retention lifecycle has no product path. Class: code gap that makes a documented customer commitment false.
src/preload/index.ts:38-40 exposes projects:close-engagement, projects:reopen-engagement, projects:record-sign-off; src/main/ipc.ts:894-957 implements them and runs the sweep on close. No file under src/renderer/src calls any of them (grep returned only a test fixture in ProjectList.test.tsx:37-38). retention-service.ts:39-55 purges only rows where closed_at is set (db-service.ts:1589-1597). Nothing sets it. docs/DATA-HANDLING.md §6.2 ("The application now enforces the part of the commitment that lives on the consultant's machine") and SECURITY.md §7 describe enforcement that a consultant cannot trigger.
Remedy: implement #118 as a bounded renderer change: an engagement panel with Close, Reopen, Record sign-off, and a countdown derived from projects:rollups (closedAt is already returned, ipc-channels.ts:112). Add a DOM test that clicking Close invokes the channel. Until it lands, amend DATA-HANDLING §6.2 to say the lifecycle actions are main-process only and not yet exposed.
F2. Key Vault rules pass vacuously on collection failure or empty collection. Class: code defect (false pass).
src/main/rules/evaluator-registry.ts:930-978 (checkKeyVaultPublicAccess, checkKeyVaultSoftDelete) iterate sub.keyVaults and return pass when the loop finds nothing. drainIter (azure-collector.ts:557-561) returns [] on failure and records the error only in snapshot.errors; these two evaluators never consult it, and they also pass when snapshot.azure is empty. Rules cis-azure-4.1.1 and 4.1.2 (resources/rules/cis-azure.json:217-243) are not in the quarantine set and carry sectionRef: "—" (unmapped legacy), so they enter the cis-azure compliance denominator with a verdict that can be "all vaults compliant" for a tenant whose Key Vault listing was denied. The storage and NSG checks in the same file (lines 737-796, 1438-1537) were hardened for exactly this; these two were missed.
Remedy: apply the same three-way pattern (unavailable, malformed, failing) as checkStorageSecureTransfer; return error when snapshot.azure is empty or any subscription's keyVaults collection is missing or errored. Add the two rules to azure-evaluator-correctness.test.ts with a denied-collection fixture. Alternatively quarantine them pending that fix; either closes the false pass.
F3. No packaged build has been produced from main, and no smoke test or live-tenant run has ever been observed. Class: missing evidence, not a code defect.
plans/00-context.md:88-89, docs/BUILD-AND-TEST.md:140-141, docs/DATA-HANDLING.md:468-470 all state it. actions-runs-40.json shows the last installer dispatch at 89f23a22 (2026-09-23) under the retired standalone workflow. The current ci.yml native jobs (lines 408-677) are workflow_dispatch/workflow_call only and have no recorded run.
Remedy: dispatch ci.yml on main once; read the four verdict.json artifacts; hand the NSIS and DMG artifacts to the human owner of #109/#237. This is the single highest-leverage evidence action available.
Required
F4. WAF evaluators report collection failure as non-compliance. Class: code defect (false negative).
evaluator-registry.ts:1583-1601 (checkNetworkSegmentation, rule waf-se-05) and 1680-1698 (checkMonitoringConfigured, rule waf-oe-02) treat a null or empty collection as fail without consulting snapshot.errors. safeFetch returns null on failure (azure-collector.ts:585-589); drainIter returns []. A missing Reader assignment on one subscription is therefore reported as "no NSGs" or "no diagnostic settings". PRs #389 and #390 fixed only the zero-subscription vacuous-pass case. docs/DATA-HANDLING.md §4 promises the opposite.
Remedy: check collectionFailed(snapshot, 'azure/<sid>/networkSecurityGroups') and 'azure/<sid>/diagnosticSettings' per subscription and return not-assessed (consistent with the sibling checkActivityLogAlert at line 1348) when any subscription is uncollected.
F5. Rules-engine initialization failure does not stop the app. Class: code defect (fail-open).
src/main/index.ts:170-175 logs and continues when rulesEngine.initialize() throws; rules-engine.ts:403-406 only warns when the bundled directory is missing. A packaged build with an empty resources/rules (the documented stale-release/ trap, README.md:349-360) loads zero rules, and assessment:run then persists zero findings and the PDF prints "0 controls were checked". Settings shows loadErrorCount, but nothing gates assessment. This is #168, labelled mvp-critical and unstarted.
Remedy: in ipc.ts:1445 reject assessment:run when rulesEngine.getStatus().ruleCount === 0 or loadErrors.length > 0, with a message naming the rules directory; in index.ts treat a missing bundled directory on a packaged build as fatal (showErrorBox and quit, as the secret-store gate does at lines 133-151).
F6. Deliverables carry no benchmark or rule-set provenance. Class: code gap (#166, mvp-critical).
export-service.ts:343-353 prints project, customer, snapshot label, generation date. The engine exposes rulesHash and rulesVersion (rules-engine.ts:346-355) and the rule files carry version and benchmarkVersion; none reach the PDF, Excel or PPTX. Only the JSON export records app.getVersion() (line 1548). A customer cannot tell which benchmark revision or rule set produced a report.
Remedy: add one provenance block to every exporter: benchmark ids with version, rulesVersion, rulesHash, app version, snapshot id. Source it from one helper so the four formats cannot disagree, and drift-test it the way coverage counts are drift-tested.
F7. The report template that meets the report standard is landed but unwired. Class: delivered code without delivered outcome.
src/main/report/template.ts and report-data.ts exist with 78 + 28 + 12 + 6 tests; grep for renderReportHtml, toReportData(, printToPDF finds no caller outside src/main/report/. ipc.ts:1560-1620 still calls pdfkit. #208 (printToPDF renderer) has no candidate. The customer-facing report is therefore the older pdfkit layout that #211 planned to retire.
Remedy: owner decision (§8): ship v1 on pdfkit plus F6 provenance, or make #208 a v1 item. If the latter, report-data.ts:344-364 must be corrected first (F8).
F8. The report data layer names permissions the tool does not request. Class: code defect in customer-facing text.
report-data.ts:344-364 maps directoryRoles to RoleManagement.Read.Directory and sensitivityLabels to InformationProtectionPolicy.Read.All. The collector and README require Directory.Read.All and SensitivityLabels.Read.All (README.md:37-44; src/renderer/src/lib/permissions-table.ts:17,43). A "resolves with" line in a report would tell the customer's admin to grant a permission the tool does not use. Not reachable by consultants until F7 is wired, so Required rather than Critical.
Remedy: derive RESOURCE_PERMISSIONS from permissions-table.ts (one source) or add a drift test that every value appears in the README table.
F9. Backlog target validation permits plaintext token transport and path-shaped identifiers. Class: security hardening gap.
ipc.ts:528-531 accepts any ^https?:// organization URL; azure-devops-adapter.ts:55,100 then sends Basic auth with the PAT to it. ipc.ts:541-546 accepts any non-empty owner/repo; github-adapter.ts:42,70 interpolates them into the API path, so an owner containing / or .. changes the endpoint the PAT is sent to. Both are consultant-supplied values on the consultant's own machine, so impact is bounded, but the PAT is the one write credential in the product.
Remedy: require https: for orgUrl and a dev.azure.com or visualstudio.com host; restrict owner/repo to ^[A-Za-z0-9_.-]+$.
F10. docs/DATA-HANDLING.md disagrees with the collector on what is read. Class: documentation defect in a customer-facing document.
Lines 65-69 say fifteen Azure resource families; azure-collector.ts:711-729,1015 collects sixteen, the sixteenth being activityLogAlerts. Lines 95-97 say activity-log alert rules are "alert rules this tool does not collect"; azure-collector.ts:1014-1020 collects them. §6.2 overstates retention enforcement (F1).
Remedy: reconcile the two sections; add a drift test that the documented resource list equals the collector's AzureSubscriptionData keys (the M365 table already has this shape).
F11. Build prerequisites are understated. Class: documentation defect.
npm test on main executes cis-authority.test.ts, which spawns python3 -B scripts/test-cis-authority.py requiring openpyxl; ci.yml:28-38 installs both before testing. docs/BUILD-AND-TEST.md:14-19 and README.md:302-309 list Node only. A consultant following the runbook on a clean laptop gets a red suite.
Remedy: either list Python 3.12 + openpyxl 3.1.5 as a test prerequisite, or make that one test skip with an explicit "environment not provisioned" status when Python is absent, never silently pass.
Optional
F12. Cancellation is not honoured on the activity-log-alert stage. azure-collector.ts:1015-1020 calls drainIter without signal, unlike every sibling call. A cancel during that page fetch waits it out. Remedy: pass signal.
F13. Secret-store writes are not serialized. key-vault-service.ts:148-193: writeEntry and deleteSecret each load the whole JSON, mutate, and atomically replace. Two concurrent handlers (for example backlog:configure racing projects:create) can lose one write. getOrCreateExportKey (snapshot-portability-service.ts:136-142) has a check-then-write race. Remedy: a single promise-chain mutex around load/save, same pattern as withLifecycleLock.
F14. Global Administrator count of zero resolves to na. evaluator-registry.ts:246-250. No real tenant has zero Global Administrators; zero means the role page shape was not what the evaluator expected. plans/00-context.md:121-124 records this as a deliberate Terra ruling, so it is a decision to revisit, not a bug to fix silently. Remedy: return error with evidence naming the role enumeration when count is zero, or keep na and say why in the finding text.
F15. Large-file and cohesion smell. src/main/ipc.ts 2111 lines, db-service.ts about 3900 lines, evaluator-registry.ts 1766 lines, export-service.ts over 700 lines with four renderers in one module. Remedy: register handlers per domain module (ipc/projects.ts, ipc/exports.ts, …) and split DbService into registry, customer-data, and artifact-reservation modules behind the same public class. No behaviour change; do it when the security-core work above is done, not before.
F16. Repository hygiene. Main carries about 150 per-issue receipt logs and reports at the root (issue233-harness-policy-receipts/*, issue233-pptx-receipts/*, issue2xx-*-report.md, cis-287-remediation-r3-report-*.md, waf-slice1-report-*.md), two vendored tarballs, a proof-of-concept tree (scripts/sqlcipher-poc) and a nested legacy harness with its own lockfile that CI installs on every push (ci.yml:333-371, about 100 s). Remedy: move receipts to docs/evidence/ or an evidence branch; keep the legacy-harness job but run it on a path filter or dispatch.
F17. Minor documentation drift. SECURITY.md:295-296 says the app never calls shell.openExternal; ipc.ts:685 does, to a fixed allowlisted URL (a private-repository page consultants may not be able to open). plans/00-context.md:93-105 still says 294/283/11 and 27 verdict-producing rules; README says 305/283/22/16 and the drift test agrees with README. SECURITY.md:216-220 says reason is optional for manual-verdict; curation-service.ts:263-265 requires it. Remedy: one docs pass; add the sectionRef "—" and Option R rules to SECURITY §4.
No remediation needed (verified sound)
- IPC boundary: UUID, length, enum and shape validation on every handler;
assertSnapshotBelongsToProjecton every project+snapshot pair (ipc.ts:107-120and call sites); import channels reject a smuggledfilePathbefore any dialog (ipc.ts:1999,2037); evidence attachments must beUint8Array(ipc.ts:611-617). - Electron hardening:
contextIsolation,nodeIntegration: false,sandbox: true, window-open deny,will-navigateorigin/path check, webview deny, header-only environment-aware CSP (index.ts:29-116); fuses set by electron-builder (package.json:128-135). - Database: envelope key wrap with AES-256-GCM (
db-service.ts:382-401); adapter pins and reads back the SQLCipher v4 profile before and after keying, rejects plaintext, short and absent files, and never creates on an open path (sqlcipher-adapter.ts:339-557); per-connection pragmas read back on every open (db-service.ts:1227-1243); migration steps atomic (1310-1325). - Retention purge: strip, re-key to a fresh file, swap the registry key before rename, crash recovery of the sibling,
purged_atwritten last (db-service.ts:1775-2069); report-file removal only through the rename-quarantine ownership boundary keyed ondev:inoplus content digest (db-service.ts:626-712, 1641-1753). - Export reservation: exclusive
wx+claim, all bytes written through the retained descriptor, identity re-checked before registration (db-service.ts:3391-3676). - Snapshot portability: unconditional AES-256-GCM with scrypt, KDF parameter bounds on import, digest before HMAC before decrypt, denylist scan on plaintext (
snapshot-portability-service.ts). - Collectors: Graph
@odata.nextLinkorigin and path pinned (m365-collector.ts:49-72), page cap, per-element validation for role members; ARM calls bounded by timeout, retry withRetry-After, concurrency cap, abortable at every stage except F12. - Compliance math: one shared
effectiveStatusand coverage aggregation (src/shared/coverage.ts) used by dashboard, registry rollup and all exporters; manual verdicts count only when evidence-qualified. - Counts that reach customers are derived:
coverage-counts.test.tsdrives the real engine and parses README;benchmark-denominators.test.tsderives 160/127 from committed manifests; the WAF manifest is byte-for-byte reproduced by its generator.
4. Priority-area deep dives
4.1 CIS baseline delivery
Observed on main: resources/rules/cis-m365.json (173 rules, version 7.0.0, "all 160 recommendations, derived from the licensed workbook … by scripts/generate-cis-rules.mjs") and cis-azure.json (136 rules, version 6.0.0, all 127). The 22 extra rules are the pre-v7/v6 legacy set declared unmapped in resources/control-indexes/ (13 M365 + 9 Azure per docs/CIS-SOURCE-AUTHORITY.md:213-214). The coverage matrix records 2 tool-automated controls (cis-m365-5.2.1, cis-m365-6.1.1), 278 manual workflows, 7 tooling gaps. README states exactly this and the drift test enforces it.
The trust chain (docs/CIS-SOURCE-AUTHORITY.md:56-118) is sound in design: an externally supplied source-binding receipt with an externally recorded SHA-256, a hash-pinned extractor, committed corpus bytes, generated rules, and a coverage matrix that the generator reproduces byte-for-byte. Two things remain by construction, not by defect:
- The approval gate:
verify-cis-authority.py --require-approvalneedsCIS_HUMAN_APPROVALS_JSONand_SHA256naming three subjects. Owner action #376. - The licensed re-extraction: hosted CI cannot hold the workbooks;
--civerifies committed bytes instead and says so. Owner action #377 on an authorized machine, or provisionCIS_LICENSED_WORKBOOKS_B64.
Open sub-issues with preserved candidates: #373 (feat/issue-373-142b-orphan-rules @ 1a317e0, 1 ahead / 54 behind), #374 (feat/issue-374-142c-severity-provenance @ 92d4e6c, 1 ahead / 54 behind), #253 parity (feat/issue-253-parity-r2 @ 97d920f). #375 is a decision on the two pending tuples in resources/cis-authority/pending-unmapped.json. #378 closes #142 when the others are done.
One design caution: the CIS gate is a required push-path job. While approval is genuinely absent, every push to main is red, and actions-runs-40.json shows that has been true for at least six days. A green typecheck/test/lint run is still visible per job, but the run-level signal is lost. Recommendation in §9.4.
4.2 WAF
Observed on main: resources/waf-baseline/waf-baseline-manifest-2026.09.json (59 items, 0 fully automated, 4 partial, 55 consultant-review), retained page bytes under sources/pages-2026-09-15/, a strict validator shared by generator and runtime import, 135 drift tests. docs/waf-baseline-reconciliation.md is honest: eleven of twelve bundled rules cite a framework code they do not address, and the correspondence file corrects the mapping without editing the rule files.
What is not done: the rule files still carry the wrong sectionRef (customer-visible), two WAF evaluators still have the false-negative class (F4), and the Security pillar port (#230) is an approved candidate awaiting merge authority: assessor/issue230-report-0498d3a1-r1 @ c4fcb028, 1 ahead / 8 behind main. Its base 0498d3a1 is the parent of main's last merge, so the rebase risk is limited to PR #391's WP3-B2 files (scripts/native-package-workflow-drift.mjs and its tests), which the candidate does not touch ("scripts/ untouched" per the owner card).
4.3 Database and encrypted DB
The cutover is complete on main: src/main/sqlcipher-adapter.ts is the only driver path; db-service.ts creates and opens through it; the corpus fixtures (test/fixtures/sqlcipher-corpus/*) were frozen from the genuine SQLCipher amalgamation and are read back through the new driver. #229, #231, #232, #233 are closed. #235 proof stack (WP1-WP4) merged via PRs #379, #380, #385. #234 is labelled ready; its substance (drop the JourneyApps devDependency so a Windows npm ci never compiles it) is on main: root package.json has no @journeyapps/sqlcipher, PR #388 asserts no override survives, and the only remaining reference is the nested legacy harness with its own lockfile. What remains for #234 is to compare its acceptance criteria against main and close.
Residual risks: (a) the IncompleteDatabaseFileError guard rejects files under one page, which is correct, but a database whose -wal sidecar holds the only committed rows is accepted and left to SQLite recovery; the adapter documents this. (b) deleteProject (db-service.ts:1484-1499) removes -journal and -wal but not -shm or a stale .purge sibling; both are ciphertext under a key that no longer exists after the registry row is gone, so cryptographic erasure holds. Nit only.
4.4 Retention and erasure
The main-process implementation is the strongest part of the codebase (see "No remediation needed"). The gap is F1: there is no user interface for close, reopen, sign-off, or countdown, so the purge can never be reached. #160 ("export artifacts can escape the purge, and purge can delete a file it did not write") is, by content, resolved on main: report rows record file_identity, removal goes through the ownership boundary, and a missing customer database fails the purge closed rather than unlinking (retention-service.ts:93-131). The open issue should be verified against its acceptance criteria and closed; the preserved branch opencode/issue-160 @ 3620ca6 is superseded by the PR #192 lineage that landed.
4.5 Reports and export integrity
Consistent compliance math across PDF, Excel, PPTX, JSON, briefing and dashboard is real (shared coverage.ts). Suppressed findings move to a dedicated section rather than disappearing; manual verdicts are labelled "(manual)" and attributed. The evidence appendix is metadata-only in PDF and Excel; attachment bytes never leave the encrypted database. The .xisnap path is encrypted-only, and the renderer drops the passphrase from state after export (ReportsPanel.tsx:245).
Gaps: F6 (no provenance), F7 (report standard unwired), F8 (wrong permission names in the new data layer). The old pdfkit report uses WinAnsi fonts, so non-Latin customer names will render as boxes; the new template self-hosts IBM Plex and Space Grotesk and has an escaping contract enforced by a grep test. That is a real reason to prefer #208 for v1 if the owner can afford it.
4.6 Packaging and CI
ci.yml on main has eight jobs on push plus three dispatch-only native jobs. Strengths: audit gate with no exception list (scripts/audit-check.mjs), dependency policy that fails on symlinked or unsupported packages (scripts/dependency-policy-core.mjs), authored-candidate checkout for the CIS jobs, native matrix derived from one policy file with per-target prebuild SHA-256 (scripts/native-package-policy.json), builder artifact manifest as the only way to locate the distributable, an aggregate gate that cannot convert blocked to pass.
Gaps: (a) the native path has never run since the seam landed (F3); (b) the inspector's supplyChain.sbomPath, licenceInventoryPath, provenancePath are requested (ci.yml:532-536) but nothing on main generates an SBOM, so supply-chain will report blocked (#169, WP5); (c) push CI is red by design (§4.1); (d) the native-package-evidence job hard-codes expectedJobId 1101-1104 (ci.yml:658-661) while matrix.jobId comes from the policy generator; a mismatch will fail the aggregate. I could not confirm the generator emits those values. (e) windows-build.yml is a thin workflow_call front door; the run history under its old name is the only installer evidence and predates the current code.
5. Complete issue inventory
Classification values: merged-complete, active-with-evidence, preserved-candidate, stalled, technically-blocked, owner-decision, owner-action, human-gate-later, superseded/duplicate, deferred-by-decision, evidence-incomplete.
5.1 Epics and MVP-critical items
| # | Title (short) | Classification | Candidate or blocker | Next action |
|---|---|---|---|---|
| 3 | Epic: verify and ship a packaged build | technically-blocked | needs F3 dispatch; children #109 #110 #111 #236 #237 #238 | dispatch native matrix on main |
| 4 | Epic: close the benchmark coverage gap | active-with-evidence | PR #261 on main; children #373-#378 | run the CIS closure chain (§9.2 step 8) |
| 115 | Epic: engagement lifecycle and retention | active-with-evidence | #116 #117 merged; #118 unstarted (F1) | open bounded renderer issue |
| 118 | Retention 3/3: close, sign-off, countdown UI | stalled (not started) | worktree claude/issue-118 at 71f6106, 0 commits ahead | implement; IPC already exists |
| 142 | Author the 287-control baseline | active-with-evidence | merged via PR #261 (106ad99c); residual in #373-#378 | keep open until #378 |
| 147 | Import a CIS release from workbook as reviewable diff | merged-complete by content | scripts/extract-cis-corpus.py, generate-cis-rules.mjs --check on main; opencode/issue-147 @ 6926adc superseded | verify acceptance criteria, close |
| 160 | Export artifacts escape purge / purge deletes foreign file | merged-complete by content | file_identity + ownership boundary on main (PR #192 lineage); opencode/issue-160 @ 3620ca6 superseded | verify, close |
| 162 | Real SDK shapes make 13 rules return false results | active-with-evidence | 13 rules quarantined on main; evaluators corrected; azure-evaluator-correctness.test.ts 46 cases; claude/issue-162 @ 6af144e superseded | un-quarantine only through #375/#376 approval |
| 164 | Evaluate every subscription; NSG and alert false-pass | technically-blocked on #162 | activity-log alerts now collected and evaluated per subscription; quarantined | close as duplicate of #162 once un-quarantined, or keep as the "prove on real data" gate |
| 166 | Provenance in every deliverable | stalled (not started) | worktree opencode/issue-166 at b568c1d, 0 ahead | implement F6 |
| 168 | Fail closed on permissions, unused IPC, unavailable rules | stalled (not started) | none | implement F5 |
| 207 | Reports 2/5: paged HTML template | active-with-evidence | template on main; PR #386 merged; claude/issue-207 @ c1294bc superseded | close #207 when #208 decision made |
| 208 | Reports 3/5: printToPDF renderer | technically-blocked | no candidate | owner decision (§8 D-7) |
| 227 | Epic: X-Centric PDF report standard | active-with-evidence | #206 done, #207 landed, #208-#210 open | decide v1 scope |
| 229 | Replace Windows-unsupported SQLCipher binding | active-with-evidence (parent) | #231-#233 closed; adapter on main | close when #234-#238 resolved, or close now and let children stand |
| 230 | Complete versioned WAF baseline | preserved-candidate + owner-decision | assessor/issue230-report-0498d3a1-r1 @ c4fcb028, APPROVED, 1 ahead / 8 behind | grant merge authority; rebase; merge |
| 234 | Encrypted DB 4/8: convert fixtures, cross-driver | preserved-candidate, mostly landed | devdep drop on main (PR #388, root manifest clean); feat/issue-234-devdep-drop @ 683ed61, feat/issue-234-r5 @ 4ea90ca, feat/issue234-boundary-r2 @ b657963 | compare acceptance criteria to main; close |
| 235 | Encrypted DB 5/8: purge, rotation, crash, residue proofs | active-with-evidence | WP1-WP4 merged (#379, #380, #385); feat/issue-235-wp4 @ 0310e36e likely superseded by PR #380 head d6f1cb35 | verify by diff; close |
| 236 | Encrypted DB 6/8: native packaging matrix and inspection | active-with-evidence | WP3-A (#383), WP3-B1 (#384), WP3-B2 (#387, #391), WP4 (#385), WP6 seam on main; WP1 42204a1 and WP2 a33b2c2 unmerged; WP5 absent | dispatch matrix; integrate WP1/WP2 if their checks report blocked |
| 3, 109 | HUMAN: smoke test packaged app | human-gate-later | needs installer | after dispatch |
| 110 | HUMAN: live tenant end to end | human-gate-later | needs installer or dev build plus tenant | after #109 |
| 111 | HUMAN: code signing | owner-action / deferred | README discloses unsigned internal builds | owner confirms unsigned v1 |
| 237 | HUMAN: NSIS + encrypted lifecycle on Windows | human-gate-later | needs NSIS artifact from dispatch | after dispatch |
| 238 | Encrypted DB 8/8: adoption and rollback gate | owner-decision | needs #237 evidence | after #237 |
5.2 Other open product issues
| # | Title (short) | Classification | Candidate or blocker | Next action |
|---|---|---|---|---|
| 5 | Epic: collection architecture (Resource Graph) | deferred-by-decision | post-mvp | none |
| 6 | Epic: consulting workflow | deferred-by-decision | post-mvp | none |
| 23 | Migrate to Resource Graph | deferred-by-decision | none | |
| 59 | Passphrase / 2FA unlock | deferred-by-decision | unlock_mode column reserved | none |
| 88 | Epic: UI redesign | deferred-by-decision | none | |
| 94 | Command palette | deferred-by-decision | opencode/issue-94-palette-core @ 5d7c783 preserved | archive branch |
| 131 | Revisit TCM at GA | deferred-by-decision | none | |
| 134 | Research four Graph sources | owner-decision (needs-decision) | claude/issue-134 @ 189fa0e (docs) | owner reads the research, decides |
| 137 | Map 63 Entra controls | preserved-candidate, stalled | opencode/issue-137 @ 38b03c0, 7 ahead / 77 behind; Sol verification card todo | decide: port docs or supersede by #142 source-derived rules |
| 139 | Map 46 non-TCM M365 controls | preserved-candidate, stalled | opencode/issue-139 @ e129186, 3 ahead / 92 behind | same as #137 |
| 140 | Map 127 CIS Azure controls to ARM providers | merged-complete | PR #244 (87baf60) landed plans/specs/azure-provider-mapping.md and TSV; opencode/issue-140 @ c2ab0fe (43 ahead) superseded | close; delete branch and ~45 review worktrees |
| 146 | Derive permission ask from rules | technically-blocked on #161 | rules carry permissions; UI table hand-maintained | after #161 |
| 161 | Wizard asks five permissions; the ask is twelve | owner-decision (needs-decision) | code and README consistently say five collector-exercised permissions | owner rules: v1 ask = collector set (current) or rule-declared set |
| 169 | Least-privilege CI, SBOM, secret scan, smoke gates | preserved-candidate, stalled | feat/issue169-overflow-r1 @ 66beb61, 6 ahead / 60 behind; CI seam partially covers permissions | re-scope to SBOM + dependency review + secret scan on top of WP6 |
| 170, 214, 215, 216 | Permission preflight 1/3-3/3 | technically-blocked | after #146 | none until #161 |
| 173 | Accessibility baseline | preserved-candidate, stalled | opencode/issue-173 @ bf63d5d, 2 ahead / 86 behind | port or drop; not v1 |
| 174 | Snapshot compare | deferred-by-decision | none | |
| 175 | Select findings before backlog export | deferred-by-decision | claude/issue-175 @ fef9d75 preserved; findingIds already accepted by IPC | archive |
| 179 | Keyboard-shortcut sheet | deferred-by-decision | feat/issue179-backlog-currentmain @ e274d72 | archive |
| 180, 181, 184 | UI redesign leaves | deferred-by-decision | none | |
| 183 | Enable Cancel run once #171 lands | technically-blocked label stale | #171 closed; collection:cancel on main | re-triage: verify CollectionRun has the button, then close or make ready |
| 188 | Claim visibility reconciler | deferred-by-decision | claude/issue-188 @ 9d8158a | archive |
| 199 | Replace unsupported npm chains | superseded/duplicate | PR #246 landed dependency policy; PRs #200/#243 closed unmerged; claude/issue-199-* and claude/pr200-* superseded | close |
| 203 | Scalable per-resource evidence | active-with-evidence | resource_facts, pagination core on main; IPC wiring candidate assessor/issue223-ipc-20260923T0045Z @ 07f175b | post-v1 unless #225 pilot is wanted |
| 209, 210 | Reports 4/5, 5/5 | technically-blocked on #208 | after D-7 | |
| 212, 213 | Future explorations | owner-decision, deferred | none | |
| 217-221 | Remediation report epic | deferred-by-decision | none | |
| 223 | Bounded cursor pagination over IPC | preserved-candidate | core merged (PR #245); 07f175b holds IPC/preload wiring not on main; feat/issue-223-pagination-core @ 2f1cecd superseded (weaker cursor auth) | retire 2f1cecd; port 07f175b post-v1 |
| 224, 226 | Evidence UI / export | deferred-by-decision | none | |
| 225 | Storage TLS evidence pilot (WAF SE:04) | technically-blocked | waf-se-04 quarantined | after #162 |
| 252 | CIS-CAT adoption | stalled (planning) | docs merged (#264) | fold into #4 |
| 253 | CIS-CAT parity and identity drift | preserved-candidate | feat/issue-253-parity-r2 @ 97d920f, 1 ahead / 54 behind | rebase and review; pairs with #378 |
| 254-258 | Post-MVP PowerShell collectors | deferred-by-decision | none | |
| 373 | 142-B orphan legacy rules | preserved-candidate | feat/issue-373-142b-orphan-rules @ 1a317e0 | rebase, review, merge |
| 374 | 142-C severity provenance | preserved-candidate | feat/issue-374-142c-severity-provenance @ 92d4e6c | rebase, review, merge |
| 375 | 142-D pending-unmapped 2.2.1/2.2.2 | owner-decision | pending-unmapped.json names both tuples | approve or reject the two tuples |
| 376 | 142-E discharge the approval gate | owner-action | needs CIS_HUMAN_APPROVALS_JSON + SHA secrets | supply, or move gate off push path |
| 377 | 142-F licensed re-extraction | owner-action | needs the two xlsx on an authorized machine | run extract-cis-corpus.py --check; record receipt |
| 378 | 142-G close #142 with residual-risk list | technically-blocked | on #373-#377 | last |
5.3 The C01-C40 and WP-001 to WP-080 batch (#265-#371, 106 issues, all created 2026-09-17)
What they are: a plan-level decomposition, produced in one sitting, of Azure collector expansion (C01-C11 storage and vault, C13-C25 monitoring and network, C27-C40 Defender, RBAC, Entra, Databricks), exact API-version projections for each provider call (WP-001 to WP-042), CIS 9.3.x bindings (WP-043 to WP-050), reconciliation of the #162 canonical-shape work (WP-051 to WP-054), the permission-ask derivation (WP-055 to WP-057), a request/receipt kernel and completeness certificates (WP-058 to WP-066), and a bounded evidence IPC pipeline (WP-067 to WP-080). None has a branch, worktree, or code on main. Dependency edges exist only in titles ("reuse #225 substrate", "with existing #162 candidate", "with the existing #171 gate").
Is the batch still the right plan? No, as issues. Three reasons:
- Part of it is done. WP-051 (remove diagnostic-setting-as-activity-alert false pass) is implemented on main (
evaluator-registry.ts:1301-1370) and quarantined behind #162. WP-052/053/054 describe #162's canonical shapes, which #162 already carries. WP-062 (RG/tag intersection) describesapplyResourceScopebehaviour that main already has (azure-collector.ts:608-642); whether it is a bug depends on a spec the issue does not cite. - Part of it is a different architecture. "Request kernel", "datapoint completeness certificates", "fact generations with atomic visible-generation switch" describe a pipeline that main does not have and that no v1 gate requires. They belong in one design issue with a spec, not 25 leaves.
- Part of it is post-MVP by its own labels (WP-002 to WP-042, WP-048 to WP-050, WP-075/076, and every C-series issue is p3).
True dependency chain for v1 (what the batch was trying to express): #162 canonical shapes proven then un-quarantined via #375/#376, then #164 verified on a live tenant (#110), then WAF evaluator honesty (F4) and the #230 port, then #166 provenance, then reports. Everything else in the batch is post-v1.
Recommendation (§9.5): keep #351-#354 open only until #162 closes and mark them duplicates; convert C01-C40 plus WP-001 to WP-042 into three post-MVP epics; convert WP-058 to WP-080 into one design issue under #203; close WP-043 to WP-050 as duplicates of the #142/#375 work; keep WP-055/057 as sub-tasks of #146.
5.4 Closed-but-actionable
| # | State | Where the content lives | Residual |
|---|---|---|---|
| 171 | closed 2026-09-16 | PR #262 (a614a942) merged; main has abortable() in both collectors, discardUnpublishedSnapshot, commit-gated createSnapshot, cancelled-run retry tokens | candidate 7fc9fa68 (owner card residual 47 lines): the named features are on main; re-measure, retire as shipped |
| 172 | closed 2026-09-16 | PR #263 (b1b715f9); diagnostics.ts on main | candidate 51678e85 referenced by the owner card sequences first; re-measure, retire |
| 201 | closed 2026-09-15 | PR #260 (c2c2cf79); workloads column, validation, scoping on main | candidate 1fb4216e: 99.6% contained per the coordinator; retire as shipped |
| 233 | closed 2026-09-15 | PR #246 cutover; PR #382 ported F1 (IncompleteDatabaseFileError on main, sqlcipher-adapter.ts:195-210) | candidate bc9a5934 (unpushed sol/issue233-serial): same F1 subject; retire as shipped |
| 372 | closed 2026-09-21 | CIS source-binding job green at main | none |
6. Recovery ledger (completed-then-stalled work)
| Issue | Preserved SHA and branch | What was completed | What gates it | Divergence from main | Cheapest path back |
|---|---|---|---|---|---|
| #230 WAF Security pillar | c4fcb028 on assessor/issue230-report-0498d3a1-r1 (worktree m365-assessor-issue230-report-0498d3a1-r1) | independent APPROVE, 0 blocking findings; +751/-117, 10 files | merge authority; two environment REDs (0600 checkout, /tmp node_modules) that reproduce on base | 8 commits behind, all PR #391 drift-guard files the candidate does not touch | owner waives env REDs as provisioning defects, rebase onto c728ea48, rerun npm test in a correctly provisioned worktree, merge |
| #236 WP1 native build receipts | 42204a1 on issue236/wp1-native-build-receipts | WP1 receipt/install/load adapters; evidence card t_9518469c | OQ-A/OQ-B rulings (owner card t_a64e1564); check-id ownership | 32 behind; WP3/WP4 merged since and touched the same inspector | dispatch the matrix first; if install and node-electron-load report blocked, port 42204a1 onto main as the adapter for those two ids |
| #236 WP2 container probe | a33b2c2 on assessor/issue236-wp2-20260924T1737Z | R2/R3/R4 closed in the container probe (plan ad6b86cf) | same as WP1 | 32 behind | same as WP1, for container and asar-native-layout |
| #253 parity r2 | 97d920f on feat/issue-253-parity-r2 | canonical-binding drift guard and corpus parser | never reviewed after PR #261 landed | 54 behind | rebase onto main; request one review; it is a test-only addition |
| #373 orphan rules | 1a317e0 on feat/issue-373-142b-orphan-rules | dispositions recorded for 20 orphan rules | review | 54 behind | rebase, review, merge |
| #374 severity provenance | 92d4e6c on feat/issue-374-142c-severity-provenance | honest severity provenance for 309 rules | review | 54 behind | rebase, review, merge |
| #142/#253 MFA Conditional Access accessor | 3abd165 on feat/issue142-mfa-accessor-r140 (and bdb95fc preserve branch); integration attempt 1a89c49 on cis/r141-pr261-mfa-integration (38 ahead) | R140 hardened decoder after R135 review | never integrated after PR #261 merged from a different head; no decoder of this shape exists in main's registry | 57 behind; PR #261 lineage diverged | decide whether an MFA-via-CA evaluator is wanted for v1 (it would be a third automated M365 control); if yes, port 3abd165 alone, not the 38-commit integration branch |
| #223 IPC wiring | 07f175b on assessor/issue223-ipc-20260923T0045Z | narrow IPC/preload/session wiring for the merged pagination core | owner card ruling; post-v1 | 31 behind | post-v1 port; small |
| #169 packaged integrity | 66beb61 on feat/issue169-overflow-r1 | main-owned allow-list preflight and transactional output | superseded in part by WP6 seam | 60 behind | re-scope #169 to SBOM + dependency-review + secret-scan; take only the allow-list logic from the branch |
| #137, #139 mapping docs | 38b03c0, e129186 | control-to-endpoint mapping tables | Sol final verification cards (todo, never run) | 77-92 behind; source-derived rules now carry permissions | decide whether the docs still add value over the generated rule fields; if not, close and archive |
| Superseded, retire without review | 1fb4216e (#201), 2f1cecd (#223 core), 7fc9fa68 (#171), 51678e85 (#172), bc9a5934 (#233), 0310e36e (#235 WP4), ac82d6b1/b8fc7f1/083ef33/7c8778a/14232b6 (#236 WP3-B2 attempts), 6af144e (#162), 3620ca6 (#160), 6926adc (#147), c2ab0fe (#140), 683ed61/4ea90ca/b657963/1f0b009/6072d22/ef270af (#234), all cis/pr261-* and cis/lineage-recon-*, cis/authority-r2-f1f2, feat/waf-security-current-main-r1, feat/waf-manifest-remediation-r1 | each is either an exact merged-PR head or an intermediate of a merged lineage | none | n/a | archive tags, delete branches after a content check |
7. Kanban and WIP reconciliation
7.1 The 323 blocked cards
Counted from kanban-active-compact.json titles (approximate where titles are the only signal):
| Category | Approx. count | Verdict |
|---|---|---|
| Mirrors of GitHub issues: "WP-0nn", "C0n", "GitHub #N" | about 110 | not blockers; delete or make read-only mirrors |
| Issue #140 loop residue (Terra/Sol/DeepSeek rows, per-control continuations) | about 45 | stale; #140 merged via PR #244 |
| Issue #142 section-batch authoring cards and Unrated loop | about 20 | stale; PR #261 landed the full baseline |
| Issue #223 loop (cursor auth rounds) | about 15 | stale; core merged, wiring is one candidate |
| PR #200 loop | about 12 | stale; superseded by PR #246 |
| #236 WP3-B2 loop (plans r3-r8, four attempts, reviews) | about 12 | stale since PR #391 (2026-09-28T03:24Z) |
| #230, #234, #235, #233, #171, #179 remediation loops | about 30 | stale except the four candidates in §6 |
| Fleet delivery program D0-D9 and "durable fleet delivery" | 11 (mixed todo/blocked) | operations, not product |
| Owner-decision cards | 3 | real; see §8 |
| Operations defect cards | 3 | real; see §7.2 |
| Genuinely blocked candidate cards (WP1 evidence, WP1-5 plan, #253 continuation, #230 port, #373/#374/#375 rows, #171 RED reproduction) | about 10 | real |
Cross-check against the live census: no card that claims a live worker matches a live process. The census shows no opencode, codex, or implementer process; the only running assessor components are the lane controller and one planner one-shot. Every "IMPLEMENTATION … controller-launched, slot implementer-N" card (for example t_66c2384e, t_9a33dff8, t_9418d70e, t_2a11ad6c, t_fa69acf6) is not live. The ready card t_3dabad21 ("Assess PR261 as the R141 MFA integration vehicle") is stale: PR #261 merged 2026-09-20. The triage card t_d8a242c4 (#230 port plan) is superseded by the approved candidate c4fcb028.
7.2 Owner-decision and operations cards
Three cards carry [OWNER DECISION] in the title; the brief's fourth is most likely t_8d7267cb ("Special owner-authorized Opus: implement complete 287-control CIS runtime baseline"), which is an authorization card, not a decision card, and is discharged by PR #261.
| Card | Claim | Still true? |
|---|---|---|
t_93b3147b D-1 (#230 c4fcb028) | approved, awaiting merge authority, two env REDs | true; recommend waive REDs (both reproduce on base and are provisioning defects) and merge after rebase |
D-2 (#235 WP4 0310e36e) | approved, awaiting integration | likely superseded: PR #380 head d6f1cb35 "register WP4 backup-proof helper in the #234 boundary allowlist" is the same subject as 0310e36e "reconcile WP4 helper-module allowlist with backup-proof helper", merged 09-22; verify by diff then retire |
D-3 (#233 bc9a5934) | review-complete, unpushed, parked | superseded: PR #382 ported F1 to main 09-25; retire as shipped |
| D-4 (#236 WP3-B2 attempt 5?) | fourth rejection, threshold crossed | stale 40 seconds after creation: card created 2026-09-28T03:23:22Z; PR #391 (WP3-B2 default-count lineage 5d4aec0d) merged 03:24:02Z. Refuse attempt 5 because the lane landed by another lineage; delete r7/r8 branches |
t_a64e1564 (#236 WP1-WP5 plan, OQ-A to OQ-F) | six rulings needed before any WP brief | partly overtaken: WP3-A/B1/B2 and WP4 merged, so check-id ownership for identity, container, asar-native-layout, cleanup, buildReceipt is decided by what landed. Rule "as landed on main"; the only live question is who owns install and node-electron-load (WP1 candidate) and supply-chain (WP5, absent) |
t_6747ba12 (#201 1fb4216e, #223 2f1cecd, #171 7fc9fa68) | three rulings | recommend (a) retire-as-shipped for all three; #223's weaker cursor auth must not be reviewed; #171's named residual features are present on main |
t_859c8243 OPERATIONS worktree provisioning (umask 0077 strips read bits; /tmp symlinked node_modules) | manufactures false REDs | consistent with the two REDs on #230 and with dependency-policy-core.mjs refusing symlinked node_modules; I could not stat the worktrees, so "true" is inferred from the card's own ls-tree/stat evidence |
t_5522d2fb OPERATIONS controller discards worker stdout | writers die undiagnosable | read of controller.py denied; unverified. The census shows no writers at all, which is consistent with the described silent-exit pattern but does not prove it |
t_30168e43 planner orchestration repair | operations; not product |
7.3 The canonical dirty checkout
/root/prj-xis-m365_azure_assesor is on docs/three-layer-architecture @ dd11065 (the merged head of PR #259; 1 ahead by ancestry, 62 behind). Its working tree has 81 modified tracked files (+22,138/-706) and 60 untracked files.
Verdict: not lost work; it is main's content sitting on a stale branch. Evidence: every one of the 60 untracked paths exists in the pristine main tree (docs/CERTIFICATE-SETUP.md, docs/CIS-SOURCE-AUTHORITY.md, resources/cis-authority/, resources/cis-source-corpus/, resources/waf-baseline/, all 17 scripts/*.py and .mjs, src/main/rules/cis-*.test.ts, src/shared/waf-baseline-manifest.ts, test/fixtures/cis-preexisting-rules-baseline.json, and so on), and they are exactly the files added to main between dd11065 and c728ea48. The modified set (ci.yml, rule JSONs, control indexes, collectors, ipc.ts, db-service.ts, renderer components) is the set main changed in the same window. The diffstat magnitude (+22k lines, mostly rule JSON and control indexes) matches PR #261's baseline landing. I could not read the files to confirm byte equality, so this is an inference at high confidence. Cheapest resolution: git stash or git diff > backup.patch, git checkout main && git pull, then git diff --stat main -- . against the stash; if the residual is empty, drop it. Nothing in this tree should be committed on docs/three-layer-architecture.
7.4 Branches and worktrees
83 branches ahead of main:
| Bucket | Count (approx.) | Examples | Action |
|---|---|---|---|
| Exact heads of merged PRs (ahead only by ancestry) | 18 | opencode/issue-163 (#193), opencode/issue-167 (#196), claude/issue-165 (#194), opencode/issue-182 (#195), opencode/issue-133-recovery (#239), claude/issue-231 (#240), opencode/issue-136-recovery (#176), opencode/issue-132-collector (#178), opencode/issue-130-ui-copy (#177), opencode/issue-144-recovery (#159), claude/issue-145 (#155), claude/issue-190 (#192), feat/waf-strict-boundary-r2 (#251), docs/cis-cat-permission-role-contract (#264), docs/three-layer-architecture + 3 wt/t_* (#259), cis/pr261-committed-source-ci-r2 (#261), assessor/issue236-wp3b1-* (#384) | delete after git log --oneline main..<branch> shows only the merged commits |
| Superseded intermediates of merged lineages | about 40 | 9 cis/pr261-*, 3 cis/lineage-recon-*, cis/authority-r2-f1f2, 5 assessor/issue236-wp3b2-*, 6 feat/issue-234-*, sol/issue233-serial, feat/issue-235-wp4, claude/issue-222, claude/issue-232, claude/pr200-*, claude/issue-199-*, opencode/issue-140, claude/issue-162, opencode/issue-160, opencode/issue-147, opencode/issue-171-cancellation, opencode/issue-172-diagnostics, claude/issue-201, feat/issue-223-pagination-core, feat/waf-*-r1, claude/issue-207 | tag archive/<branch> then delete |
| Preserved candidates worth keeping (§6) | 10 | c4fcb028, 42204a1, a33b2c2, 97d920f, 1a317e0, 92d4e6c, 3abd165, 07f175b, 66beb61, 38b03c0/e129186 | keep, rebase when scheduled |
| Post-MVP feature candidates | 6 | opencode/issue-94-palette-core, claude/issue-175, feat/issue179-backlog-currentmain, claude/issue-188, opencode/issue-173, claude/issue-134 | tag and delete; the tag preserves them |
316 worktrees: 1 canonical, 1 this review's pristine main, about 85 under /root/Working/Projects (issue worktrees), about 200 under /root/Working/Reviews (detached review checkouts, of which about 45 are #140 rounds alone), 10 under /root/Working/Operations, 5 under .worktrees, 2 under /tmp. Every detached review worktree is disposable once its review report is archived. Estimated cleanup scope: about 290 worktrees removable (git worktree remove or prune after deleting directories), about 60 branches deletable after tagging, leaving about 10 candidate branches and their worktrees.
8. What requires the owner now
Decisions
| Id | Decision | Evidence it is ready | What resumes |
|---|---|---|---|
| D-1 | Grant merge authority for #230 c4fcb028; waive the two environment REDs as provisioning defects | independent APPROVE recorded; REDs reproduce on base; scripts/ untouched | WAF Security pillar lands; README counts re-derived |
| D-2 | Retire 1fb4216e (#201), 2f1cecd (#223), 7fc9fa68 (#171), 51678e85 (#172), bc9a5934 (#233), 0310e36e (#235 WP4) as shipped-on-main after a one-line re-measure each | §5.4 and §6 | two writer slots free; owner cards close |
| D-3 | Refuse a fifth #236 WP3-B2 attempt; delete r7/r8 branches | PR #391 landed the lane 2026-09-28T03:24Z | WP3-B2 closed |
| D-4 | Rule OQ-A to OQ-F "as landed on main"; assign install/node-electron-load to the WP1 candidate and supply-chain to a new WP5 | WP3 and WP4 merged | WP1/WP2 port can be briefed |
| D-5 | #375: approve or reject the two pending Azure tuples (cis-azure-2.2.1, 2.2.2) | tuples and fingerprints in resources/cis-authority/pending-unmapped.json | the approval file can be written |
| D-6 | #161: v1 permission ask is the five collector-exercised Graph permissions plus Reader (current code and README), or the rule-declared set | README, permissions-table.ts, rule permissions fields | #146, #170 chain unblocks or closes |
| D-7 | v1 report: pdfkit plus provenance (F6) now, and the report standard (#208-#210) after v1; or #208 in v1 | template and data layer on main, untested end to end | #227 epic scope fixed |
| D-8 | Ship v1 unsigned for internal use (README already says so) or fund certificates (#111) | docs/SIGNING.md | #111 closes or schedules |
| D-9 | CIS approval gate: supply approvals and workbook secrets, or move cis-source-authority to workflow_dispatch so push CI can be green | ci.yml:202-331 | main CI becomes a regression signal |
| D-10 | Retire the C/WP batch into epics per §5.3 | none of the 106 has code | board and issue list shrink by about 100 |
Actions
| Id | Action | Owner |
|---|---|---|
| A-1 | Dispatch gh workflow run ci.yml --ref main; collect the four verdicts and the NSIS/DMG artifacts | owner or release engineer |
| A-2 | Write CIS_HUMAN_APPROVALS_JSON naming the three subjects, record its SHA-256 out of band, set both as repository secrets (after D-5) | owner, on an authorized machine |
| A-3 | Run extract-cis-corpus.py --check against the two licensed workbooks on an authorized machine; optionally set CIS_LICENSED_WORKBOOKS_B64 (#377) | owner |
| A-4 | Fix worktree provisioning (umask 022, copy node_modules, post-provision assertion) and persist worker stdout before dispatching any writer | operations owner |
| A-5 | Human gates in order: #109 smoke test on macOS and Windows, #237 NSIS lifecycle, #110 live tenant | the consultant with a laptop and a tenant |
Human gates later
#110 needs a customer or lab tenant with the five Graph permissions and Reader; #238 needs #237's evidence; #111 only if D-8 chooses signing.
Resolved asks (no action needed)
The CIS 287-control baseline (t_8d7267cb authorization), the SQLCipher cutover escalation (#233 F1), the WAF denominator pin (#251), workload selection (#201), cancellation and retry (#171), diagnostics (#172), and the encrypted-DB proof stack WP1-WP4 (#235) are on main.
9. Forward plan to complete v1
9.1 Definition of Done for v1
Derived from README "Current limitations", plans/00-context.md, epics #3/#4/#115/#227 and the mvp-critical label. v1 is done when every line below is true and recorded.
- A Windows x64 NSIS installer and macOS x64/arm64 DMGs are produced by the
ci.ymlnative matrix from a main SHA, with the fourverdict.jsonartifacts attached to the release (#3, #236). SECURITY.md§9 checklist executed and recorded on Windows and macOS from those installers, including: window opens undersandbox: true; bundled rules present; delete-after-restart leaves no data; secret store entries created and removed; IPC rejects a bad UUID (#109).- One live tenant assessed end to end with the documented permission set: Global Administrator count non-zero; a run without
AuditLog.Read.Allreportserror, not a finding; a tenant with more than 999 users pages correctly (#110). - Windows encrypted-data lifecycle validated: create, collect, close app, reopen, delete, no plaintext
SQLite format 3header inuserData(#237). - Every unquarantined automated evaluator returns
errorornot-assessed, neverpassorfail, when its input collection failed or is absent (F2, F4 fixed;azure-evaluator-correctness.test.tscovers 4.1.1, 4.1.2, waf-se-05, waf-oe-02). - Assessment refuses to run with zero loaded rules or any rule-file load error; a packaged build with an empty
resources/rulesfails visibly (#168, F5). - Every PDF, Excel, PPTX, JSON and briefing states benchmark versions,
rulesVersion,rulesHash, app version and snapshot id (#166, F6), drift-tested. - Engagement close, reopen, sign-off and a retention countdown are reachable in the UI; a DOM test proves the channel is invoked;
DATA-HANDLING.md§6.2 is true (#118, F1). - CIS: #373, #374 merged; #375 ruled; approval evidence supplied or the gate reclassified (#376); licensed re-extraction receipt recorded (#377); #142 closed with a residual-risk list (#378). README counts re-derived by the drift test.
- WAF: #230 Security pillar merged;
waf.jsonsectionRefvalues corrected to the manifest'sitemIdor the rule file states they are historical; README WAF counts re-derived. - #160 and #234 verified against main and closed; #229 and #235 closed.
- Docs reconciled: DATA-HANDLING resource lists (F10), BUILD-AND-TEST prerequisites (F11), SECURITY §4 statements (F17),
plans/00-context.mdcounts. - Push CI on main is green (D-9), and the release SHA's run is green end to end.
- Owner has ruled D-6, D-7, D-8 and the outcomes are reflected in README.
9.2 Critical path
Each step: role, inputs, action, expected output, verification, stop condition, dependency.
| # | Step | Role | Inputs | Output | Verification | Stop when | Depends on |
|---|---|---|---|---|---|---|---|
| 1 | Dispatch native matrix | release engineer | main c728ea48 | 4 distributables, 4 verdicts, aggregate | native-package-aggregate artifact present; list blocked check ids | artifacts downloaded | none |
| 2 | Decide CIS gate (D-9) and apply | owner + one engineer | ci.yml, secrets | either secrets set or job moved to dispatch | next push to main is green | green run observed | none |
| 3 | Fail-closed evaluator fix (F2, F4) | implementer | evaluator-registry.ts, correctness tests | PR with 4 evaluators hardened + tests | npm test; coverage-counts drift test still green (counts may change; README updated in the same PR) | merged | none |
| 4 | Merge #230 port | implementer + reviewer | c4fcb028, correctly provisioned worktree | rebased PR | full suite green in a 0644-mode worktree | merged | D-1, A-4 |
| 5 | Provenance in exports (F6) | implementer | export-service.ts, engine status | one provenance helper, four exporters, drift test | test proves each format contains rulesHash | merged | none (conflicts with step 3 only in README) |
| 6 | Zero-rules fail closed (F5) | implementer | index.ts, ipc.ts | startup fatal on packaged build without rules; assessment:run refuses | unit tests for both | merged | none |
| 7 | Retention UI (#118, F1) | implementer (renderer) | ipc-channels.ts, ProjectWorkspace.tsx | engagement panel + DOM tests; DATA-HANDLING §6.2 amended | DOM test invokes each channel; manual check in dev | merged | none |
| 8 | CIS closure chain | implementer + reviewer + owner | 1a317e0, 92d4e6c, 97d920f, D-5, A-2, A-3 | #373, #374, #253 merged; approvals file; receipt | check:cis-approval green locally with the supplied files | #378 closed | D-5, A-2, A-3 |
| 9 | Human gates | consultant | installers from step 1 (rebuilt after steps 3-7) | filled §9 checklist, live-tenant report, Windows lifecycle log | evidence attached to #109/#110/#237 | all three pass | 1, 3, 6 |
| 10 | Report decision (D-7) and, if chosen, #208 | owner then implementer | template, report-data.ts, F8 fix | printToPDF exporter behind export:pdf | golden-file test; manual render | merged or deferred | D-7 |
| 11 | Docs reconciliation (F10, F11, F17) | technical writer or implementer | findings list | one docs PR with drift tests where cheap | tests green | merged | 3, 5, 7 |
| 12 | Board and branch hygiene (§9.5) | coordinator | this report | closed cards, tagged and deleted branches, pruned worktrees | board shows about 10 blocked | done | D-2, D-3, D-10 |
| 13 | Release | owner | green main run, human evidence | tagged v1 release with artifacts and verdicts | DoD list checked | released | all |
9.3 Parallel lanes (no file-conflict risk)
- Lane A, evidence: steps 1, 2, 9. Touches no source files.
- Lane B, compliance core: steps 3, 5, 6. Files:
evaluator-registry.ts,azure-evaluator-correctness.test.ts,export-service.ts,index.ts,ipc.ts(assessment handler only), README counts. One implementer, sequential inside the lane. - Lane C, retention UI: step 7. Files:
src/renderer/**,docs/DATA-HANDLING.md§6.2 only. No overlap with B. - Lane D, CIS closure: step 8. Files:
resources/control-indexes/*,resources/cis-authority/*,src/main/rules/control-index-drift.test.ts, rule JSONseverityfields. Overlaps B only if B quarantines or un-quarantines rules; sequence: B first (quarantine list unchanged), D second. - Lane E, WAF: step 4. Files per the candidate (WAF rules, manifest, correspondence, reports). Overlaps B in
evaluator-registry.tsonly if the candidate edits WAF evaluators; the card says it re-ports "Security SE:06-SE:12 availability", which toucheswaf.jsonand the manifest, so merge B's F4 fix first and rebase E. - Lane F, hygiene: step 12. No source files.
9.4 Unblock plan
| Item | Exact decision or act | Ready evidence | Resumes |
|---|---|---|---|
| CIS gate red | D-9. Recommended: keep cis-source-authority as a required job only on workflow_dispatch and on a release/* branch; keep cis-source-binding and cis-authority (committed-bytes) on push. Rationale: the gate is human-only by design and cannot be satisfied by code | ci.yml:202-331; failure log | push CI becomes a regression signal; release still requires the gate |
| #375 | approve tuples d2586e1c… and 10e5cd71… or reject with reason | pending-unmapped.json | approvals file can name them |
| #376 | write approvals JSON with approvedSubjects for the two rules and sourceDerived, each with sourceBindingSha256, approver, approvedAt; record SHA-256 out of band; set two secrets | docs/CIS-SOURCE-AUTHORITY.md:266-285 | check:cis-approval passes |
| #377 | run the licensed --check on an authorized machine; commit nothing; record the receipt path in #377 | extractor and workbooks | #378 |
| #161 | D-6 | current five-permission ask | #146, #170 chain |
| #208 | D-7 | template on main | #209, #210 |
| #111 | D-8 | SIGNING.md | close or schedule |
| #238 | after #237 evidence: adopt, or roll back to the legacy harness path | none yet | #229 epic closes |
| Owner cards | D-1 to D-4 | §7.2 | four writer slots free |
| Worktree REDs | A-4 | operations card | candidate gates can be trusted again |
9.5 Board hygiene plan (list only; nothing closed by this review)
Close as stale on the board (about 250 cards): every "GitHub #N" mirror whose issue is closed or deferred; every #140, #142-section, #223-loop, PR #200, #236-WP3-B2 (r3-r8 plans, attempts 1-4, reviews), #234-r1..r5, #233, #171, #179, #94 loop card; the ready card t_3dabad21; the triage card t_d8a242c4; all [assessor] Issue #N delivery → Terra review and Sol final verification pairs for merged issues (#140, #147, #162, #166 remains open, #169 remains open, #173, #179, #180, #181, #199, #207, #118 remains open).
Merge into fewer real work packages: the ten §6 candidates become ten cards, one each, with the SHA in the title and nothing else; the D0-D9 fleet program moves to an operations board; the three owner-decision cards collapse into one card listing D-1 to D-10 with checkboxes.
Propose closing on GitHub (owner to confirm): #140, #147, #160, #199 as complete-by-content; #183 after a Cancel-button check; #351 as implemented and duplicate of #162; #352-#354, #343-#347 as duplicates of #162/#375; #265-#300, #302-#342, #348-#350, #370-#371 folded into three post-MVP epics; #355-#369 folded into one design issue under #203; #252 folded into #4; #229 once #238 is decided; #234 and #235 after the diff checks in §6.
9.6 Risks and evidence gaps
Covered in §10.
9.7 Recommended first five actions today
- Rule on
t_6747ba12andt_93b3147busing D-1 to D-4: retire six candidates as shipped, approve the #230 merge with the environment REDs waived, refuse a fifth WP3-B2 attempt. - Dispatch
ci.ymlon main and read the four native verdicts; this is the only way to learn what WP1/WP2/WP5 still owe. - Decide D-9 and either set the CIS approval secrets or move the gate off the push path, so tomorrow's pushes are green or red for a reason.
- Open two
readyissues with the file lists from §9.3: "fail-closed on collection failure for cis-azure-4.1.1/4.1.2, waf-se-05, waf-oe-02" and "#118 engagement lifecycle UI"; both are one-day bounded changes with existing tests to extend. - Fix worktree provisioning (
umask 022, copiednode_modules, post-provision assertion) before any writer is dispatched; until then every full-suite RED is uninterpretable.
10. Risks and evidence gaps
| Risk or gap | Why it matters | What closes it |
|---|---|---|
No packaged run ever observed (sandbox: true, fuses, rules bundled, secret store on Windows/macOS) | every "fixed" in the docs is unobserved on the platforms consultants use | steps 1 and 9 |
| No live-tenant run | H2/H3/H4 fixes (role paging, error-versus-negative, Graph paging) unproven against Microsoft's real shapes; #162's quarantine can only be lifted with such data | #110 |
| Windows native driver behaviour | prebuild SHA pinned, but load-in-Electron on win32 not observed; sqlcipher-adapter profile read-back could differ | #237; native matrix node-electron-load check |
| Push CI red by design | masks regressions; the coordinator already reads per-job results, humans may not | D-9 |
| Test suite depends on Python and file modes | false REDs consumed reviewer rounds on #230; a clean laptop fails the runbook | F11, A-4 |
Aggregate job id mismatch (ci.yml:658-661 literals vs policy-derived matrix.jobId) | a first dispatch may fail in the aggregate for a bookkeeping reason | inspect native-package-matrix.mjs output on the first dispatch |
| No SBOM producer despite the inspector request | supply-chain check reports blocked; #169 open | WP5 or re-scoped #169 |
| Unrated severity on 277 rules | reports and backlog priority treat unrated as a distinct bucket; the DevOps adapter maps no priority for it (azure-devops-adapter.ts:27-33), which is correct but means most exported work items carry no priority | document in README backlog section; owner decides whether a default priority is wanted |
| Legacy pdfkit report and WinAnsi fonts | non-Latin customer names render as boxes | D-7 |
| Board state divergence | 323 blocked cards with about 10 real blockers; decisions written in reconciliation reports rather than cards | §9.5 |
| The MFA Conditional Access accessor lineage | 38-commit integration branch diverged from what merged; may hold an evaluator that was reviewed to R140 and then dropped | §6 decision on 3abd165 |
| I could not read the canonical checkout or the lane controller | the dirty-tree verdict and the operations card are inferred | one git stash and one read of controller.py:583 by someone with access |
11. Appendix
11.1 Evidence paths used
All under /root/Working/Reviews/assessor-full-portfolio-fable-20260928/:
inputs/brief.md,inputs/manifest.jsonevidence/main-c728ea48e71674e8e4b052b7b16d5933a32a080f/(pristine main):README.md,SECURITY.md,CLAUDE.md,HANDOFF.md,REVIEW.md,package.json,plans/00-context.md,plans/01-work-tracking.md,plans/AGENTS.md,plans/COORDINATOR.md,docs/ARCHITECTURE.md,docs/BUILD-AND-TEST.md,docs/DATA-HANDLING.md,docs/CIS-SOURCE-AUTHORITY.md,docs/waf-baseline-reconciliation.md,docs/SIGNING.md,.github/workflows/ci.yml,.github/workflows/windows-build.yml,src/main/index.ts,src/main/ipc.ts,src/preload/index.ts,src/shared/ipc-channels.ts,src/shared/coverage.ts,src/main/db-service.ts,src/main/sqlcipher-adapter.ts,src/main/key-vault-service.ts,src/main/retention-service.ts,src/main/engagement-folder.ts,src/main/snapshot-portability-service.ts,src/main/export-service.ts(first 700 lines),src/main/report/report-data.ts(first 400 lines),src/main/report/template.ts(first 200 lines),src/main/github-adapter.ts,src/main/azure-devops-adapter.ts,src/main/curation-service.ts(grep),src/main/collector/collector-service.ts,src/main/collector/azure-collector.ts,src/main/collector/m365-collector.ts,src/main/rules/rules-engine.ts,src/main/rules/rules-loader.ts,src/main/rules/rules-engine-singleton.ts,src/main/rules/types.ts,src/main/rules/evaluator-registry.ts,src/main/rules/coverage-counts.test.ts,src/main/rules/cis-quarantine.test.ts,resources/rules/*.json,resources/cis-authority/pending-unmapped.json,resources/cis-coverage-matrix.json(grep),scripts/dependency-policy-check.mjs,scripts/dependency-policy-core.mjs,scripts/audit-check.mjs,scripts/native-package-policy.json,scripts/inspect-native-package.mjs(first 150 lines),scripts/native-package-workflow-drift.mjs(lines 150-268),scripts/sqlcipher-legacy-harness/package.json(grep),src/renderer/**(grep only)evidence/issues-compact.txt,open-issues-full.json,closed-issues-full.json,prs-closed-all.jsonevidence/kanban-active-compact.json,kanban-all-active.json(grep and five card bodies)evidence/wip-branch-census.json,worktree-list.txtevidence/canonical-dirty-status.txt,canonical-checkout-status.txt,canonical-dirty-diffstat-full.txt,canonical-untracked.txt,canonical-ahead-commits.txtevidence/ci-main-jobs.json,ci-main-failure-log.txt,actions-runs-40.jsonevidence/main-log-40.txt,main-merges-30.txt,main-tree-files.txtevidence/live-process-census.txt
11.2 Commands used
Read-only tool calls only: Read on the files above, Grep for the counts and cross-references cited (quarantine markers, fn evaluator references, JMESPath rules, rule ids, it(/test( cases, renderReportHtml/toReportData/printToPDF callers, engagement-lifecycle channel callers in the renderer, toolAutomated/toolingGap totals, owner-decision card titles, done-card count), Glob for .github/**. No git command, no test run, no install, no write outside this file.
11.3 Unavailable evidence
/root/prj-xis-m365_azure_assesor/**(read denied): canonical checkout content./root/Working/Operations/assessor-lane-controller/**(read denied):controller.pyand any worker logs.- Job logs for main CI runs before 2026-09-28 (only conclusions were captured).
- Output of any native package dispatch (none has run).
kanban-all-active.jsondone-card bodies were not read individually (481 cards); the recovery ledger relies on branch, PR and owner-card evidence instead.- Byte-level diffs between candidate SHAs and main.
FABLE-REVIEW-COMPLETE