@aperant/framework 0.6.7 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +240 -0
- package/agents/apt-planner.md +12 -0
- package/agents/apt-pr-review-fixer.md +13 -9
- package/bin/apt-tools.mjs +7 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -1
- package/dist/cli/ci-watch/stop-matrix.mjs +16 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts +11 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +147 -3
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/features-audit.d.mts +24 -0
- package/dist/cli/commands/features-audit.d.mts.map +1 -1
- package/dist/cli/commands/features-audit.mjs +159 -5
- package/dist/cli/commands/features-audit.mjs.map +1 -1
- package/dist/cli/commands/health-check.d.mts +16 -0
- package/dist/cli/commands/health-check.d.mts.map +1 -1
- package/dist/cli/commands/health-check.mjs +119 -3
- package/dist/cli/commands/health-check.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +19 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +143 -8
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +11 -0
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts +13 -0
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +37 -2
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +132 -5
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/validate-evidence.d.mts +24 -2
- package/dist/cli/commands/validate-evidence.d.mts.map +1 -1
- package/dist/cli/commands/validate-evidence.mjs +154 -17
- package/dist/cli/commands/validate-evidence.mjs.map +1 -1
- package/dist/cli/commands/vitest-doctor.d.mts +2 -0
- package/dist/cli/commands/vitest-doctor.d.mts.map +1 -0
- package/dist/cli/commands/vitest-doctor.mjs +168 -0
- package/dist/cli/commands/vitest-doctor.mjs.map +1 -0
- package/dist/cli/config/gitignore-drift.d.mts +23 -1
- package/dist/cli/config/gitignore-drift.d.mts.map +1 -1
- package/dist/cli/config/gitignore-drift.mjs +81 -3
- package/dist/cli/config/gitignore-drift.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +56 -2
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +192 -2
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +7 -0
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +24 -2
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.d.mts.map +1 -1
- package/dist/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/dist/cli/gate/gates/gitignore-in-sync.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts +5 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +23 -18
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/gate/gates/verify-approved.d.mts +49 -1
- package/dist/cli/gate/gates/verify-approved.d.mts.map +1 -1
- package/dist/cli/gate/gates/verify-approved.mjs +93 -14
- package/dist/cli/gate/gates/verify-approved.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +8 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +122 -9
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +132 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts +38 -0
- package/dist/cli/install/legacy-paths.d.mts.map +1 -0
- package/dist/cli/install/legacy-paths.mjs +69 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -0
- package/dist/cli/install/runtime-detect.d.mts +13 -0
- package/dist/cli/install/runtime-detect.d.mts.map +1 -1
- package/dist/cli/install/runtime-detect.mjs +9 -0
- package/dist/cli/install/runtime-detect.mjs.map +1 -1
- package/dist/cli/install/runtime-migrate.d.mts +84 -0
- package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
- package/dist/cli/install/runtime-migrate.mjs +244 -0
- package/dist/cli/install/runtime-migrate.mjs.map +1 -0
- package/dist/cli/route/drift-detect.d.mts +20 -0
- package/dist/cli/route/drift-detect.d.mts.map +1 -0
- package/dist/cli/route/drift-detect.mjs +107 -0
- package/dist/cli/route/drift-detect.mjs.map +1 -0
- package/dist/cli/task/index-md.d.mts.map +1 -1
- package/dist/cli/task/index-md.mjs +14 -2
- package/dist/cli/task/index-md.mjs.map +1 -1
- package/dist/cli/util/aperant-section.d.mts +34 -0
- package/dist/cli/util/aperant-section.d.mts.map +1 -0
- package/dist/cli/util/aperant-section.mjs +127 -0
- package/dist/cli/util/aperant-section.mjs.map +1 -0
- package/dist/cli/util/copy.d.mts +28 -1
- package/dist/cli/util/copy.d.mts.map +1 -1
- package/dist/cli/util/copy.mjs +43 -55
- package/dist/cli/util/copy.mjs.map +1 -1
- package/dist/cli/util/semver.d.mts +17 -0
- package/dist/cli/util/semver.d.mts.map +1 -0
- package/dist/cli/util/semver.mjs +29 -0
- package/dist/cli/util/semver.mjs.map +1 -0
- package/dist/cli/util/skill-installs.d.mts +65 -9
- package/dist/cli/util/skill-installs.d.mts.map +1 -1
- package/dist/cli/util/skill-installs.mjs +130 -21
- package/dist/cli/util/skill-installs.mjs.map +1 -1
- package/dist/cli/util/version-preflight.d.mts +44 -0
- package/dist/cli/util/version-preflight.d.mts.map +1 -0
- package/dist/cli/util/version-preflight.mjs +66 -0
- package/dist/cli/util/version-preflight.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +11 -2
- package/dist/plugin/agents/apt-improver.md +99 -0
- package/dist/plugin/agents/apt-planner.md +127 -10
- package/dist/plugin/agents/apt-pr-review-fixer.md +13 -9
- package/dist/plugin/skills/apt/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +63 -1
- package/dist/plugin/skills/apt-debug/SKILL.md +39 -6
- package/dist/plugin/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/dist/plugin/skills/apt-diagram/SKILL.md +378 -0
- package/dist/plugin/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +72 -5
- package/dist/plugin/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/dist/plugin/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +57 -5
- package/dist/plugin/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/dist/plugin/skills/apt-improve/DEEPENING.md +84 -0
- package/dist/plugin/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/dist/plugin/skills/apt-improve/LANGUAGE.md +104 -0
- package/dist/plugin/skills/apt-improve/SKILL.md +141 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +171 -4
- package/dist/plugin/skills/apt-plan/adapters/conductor.md +98 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +57 -18
- package/dist/plugin/skills/apt-prototype/LOGIC.md +109 -0
- package/dist/plugin/skills/apt-prototype/SKILL.md +143 -0
- package/dist/plugin/skills/apt-prototype/UI.md +90 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +49 -8
- package/dist/plugin/skills/apt-release-notes/SKILL.md +193 -0
- package/dist/plugin/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/dist/plugin/skills/apt-review/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +32 -4
- package/dist/plugin/skills/apt-setup/SKILL.md +308 -6
- package/dist/plugin/skills/apt-ship/SKILL.md +122 -1
- package/dist/plugin/skills/apt-spar/SKILL.md +315 -0
- package/dist/plugin/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/dist/plugin/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/dist/plugin/skills/apt-triage/SKILL.md +169 -0
- package/dist/plugin/skills/apt-update/SKILL.md +77 -10
- package/dist/plugin/skills/apt-verify/SKILL.md +3 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +10 -5
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +166 -0
- package/dist/plugin/skills/apt-zoom-out/SKILL.md +130 -0
- package/package.json +133 -133
- package/prompts/conductor-framework-context.md +63 -0
- package/prompts/conductor-system.md +11 -0
- package/skills/apt-close-task/SKILL.md +6 -0
- package/skills/apt-discuss/SKILL.md +47 -5
- package/skills/apt-execute/SKILL.md +9 -0
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-pr-review/SKILL.md +11 -2
- package/skills/apt-quick/SKILL.md +19 -8
- package/skills/apt-researcher.md +1 -0
- package/skills/apt-setup/SKILL.md +33 -2
- package/skills/apt-ship/SKILL.md +16 -4
- package/skills/apt-spar/SKILL.md +36 -11
- package/skills/apt-update/SKILL.md +26 -1
- package/skills/apt-verify-proof/SKILL.md +7 -5
- package/skills/apt-watch-ci/SKILL.md +4 -1
- package/src/cli/ci-watch/stop-matrix.mjs +17 -0
- package/src/cli/commands/ci-watch.mjs +152 -3
- package/src/cli/commands/features-audit.mjs +164 -5
- package/src/cli/commands/health-check.mjs +116 -3
- package/src/cli/commands/init.mjs +154 -6
- package/src/cli/commands/modes.mjs +11 -0
- package/src/cli/commands/pr-review-audit-fixer.mjs +18 -5
- package/src/cli/commands/route.mjs +38 -2
- package/src/cli/commands/task.mjs +132 -5
- package/src/cli/commands/validate-evidence.mjs +158 -17
- package/src/cli/commands/vitest-doctor.mjs +173 -0
- package/src/cli/config/gitignore-drift.mjs +74 -3
- package/src/cli/config/load.mjs +188 -2
- package/src/cli/consistency/parse-review.mjs +6 -0
- package/src/cli/dispatch.mjs +23 -2
- package/src/cli/gate/gates/gitignore-in-sync.mjs +5 -2
- package/src/cli/gate/gates/review-clean.mjs +24 -19
- package/src/cli/gate/gates/verify-approved.mjs +97 -14
- package/src/cli/help.mjs +8 -2
- package/src/cli/host/detect.mjs +135 -0
- package/src/cli/install/legacy-paths.mjs +69 -0
- package/src/cli/install/runtime-detect.mjs +9 -0
- package/src/cli/install/runtime-migrate.mjs +252 -0
- package/src/cli/route/drift-detect.mjs +107 -0
- package/src/cli/task/index-md.mjs +15 -2
- package/src/cli/util/aperant-section.mjs +136 -0
- package/src/cli/util/copy.mjs +43 -56
- package/src/cli/util/semver.mjs +28 -0
- package/src/cli/util/skill-installs.mjs +134 -21
- package/src/cli/util/version-preflight.mjs +65 -0
- package/templates/aperant-claude-md-appendix.md +37 -0
- package/templates/config.json +2 -7
- package/workflows/verify-proof.md +8 -3
|
@@ -668,10 +668,14 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
|
|
|
668
668
|
}
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
-
function readFixesAppliedForFixer(reviewDir, iteration, fixer) {
|
|
672
|
-
//
|
|
673
|
-
// fixer-
|
|
674
|
-
//
|
|
671
|
+
export function readFixesAppliedForFixer(reviewDir, iteration, fixer) {
|
|
672
|
+
// BUG-030: strict-ordered candidate list. Parallel fixers write to
|
|
673
|
+
// distinct fixer-named markdowns (fixes-applied-fixer-{N}.md AND/OR
|
|
674
|
+
// fixes-applied-{fixer}.md); the legacy group-named form remains for
|
|
675
|
+
// backward-compat with reviews started before this fix. fixes-applied.md
|
|
676
|
+
// (unscoped) stays last as the legacy single-fixer fallback. NO broad
|
|
677
|
+
// glob — false-positives on the wrong fixer's file are what caused the
|
|
678
|
+
// `hallucinated` flake (4 sightings in 3 days per spec-gaps).
|
|
675
679
|
const iterDir = join(reviewDir, 'iterations', String(iteration))
|
|
676
680
|
const candidates = []
|
|
677
681
|
if (fixer === 'self-reviewer') {
|
|
@@ -681,7 +685,9 @@ function readFixesAppliedForFixer(reviewDir, iteration, fixer) {
|
|
|
681
685
|
const match = /^fixer-(\d+)$/.exec(fixer)
|
|
682
686
|
if (match) {
|
|
683
687
|
candidates.push(join(iterDir, `fixes-applied-group${match[1]}.md`))
|
|
688
|
+
candidates.push(join(iterDir, `fixes-applied-fixer-${match[1]}.md`))
|
|
684
689
|
}
|
|
690
|
+
candidates.push(join(iterDir, `fixes-applied-${fixer}.md`))
|
|
685
691
|
candidates.push(join(iterDir, 'fixes-applied.md'))
|
|
686
692
|
}
|
|
687
693
|
for (const c of candidates) {
|
|
@@ -802,7 +808,14 @@ export function cmdPrReviewAuditFixer(projectDir, extraArgs) {
|
|
|
802
808
|
|
|
803
809
|
const worktreeArg = flags.get('worktree')
|
|
804
810
|
if (!worktreeArg) return err('Required: --worktree <path>')
|
|
805
|
-
|
|
811
|
+
// BUG-015: resolve relative to the passed projectDir (not process.cwd()).
|
|
812
|
+
// Callers invoke audit-fixer from a non-worktree cwd (the main repo) with
|
|
813
|
+
// a relative --worktree like `../worktrees/foo`; resolving against cwd
|
|
814
|
+
// returned a bogus path that didn't exist, causing the audit to see zero
|
|
815
|
+
// staged files and false-positive `hallucinated`. The existing `cwd:
|
|
816
|
+
// worktree` at the spawn site (line 437-473) already scopes git
|
|
817
|
+
// invocations correctly, so no `git -C` change is needed.
|
|
818
|
+
const worktree = resolve(projectDir, worktreeArg)
|
|
806
819
|
|
|
807
820
|
const statusLineArg = flags.get('status-line')
|
|
808
821
|
// Missing / empty status line is fail-closed to hallucinated, not a
|
|
@@ -26,16 +26,36 @@
|
|
|
26
26
|
|
|
27
27
|
import { existsSync, readFileSync } from 'node:fs'
|
|
28
28
|
import { homedir } from 'node:os'
|
|
29
|
-
import { join, resolve } from 'node:path'
|
|
29
|
+
import { dirname, join, resolve } from 'node:path'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
30
31
|
import { getTaskIsolationConfig, getTrustedSkillSources } from '../config/load.mjs'
|
|
31
32
|
import '../gate/gates/index.mjs'
|
|
32
33
|
import * as gateRegistry from '../gate/registry.mjs'
|
|
33
34
|
import { detectHost, SUPPORTED_HOSTS } from '../host/detect.mjs'
|
|
35
|
+
import { detectSourceWorkspaceDrift } from '../route/drift-detect.mjs'
|
|
34
36
|
import { buildEnvelope } from '../route/envelope.mjs'
|
|
35
37
|
import { discoverSkills } from '../route/skill-discover.mjs'
|
|
36
38
|
import { executionModesList, isValidExecutionMode } from '../task/execution-mode.mjs'
|
|
37
39
|
import { err, exitWith, ok } from '../util/result.mjs'
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Read the installed kernel's own version from its package.json.
|
|
43
|
+
* Same pattern as commands/check-version.mjs:108-109 — `__dirname` of
|
|
44
|
+
* the running module resolves to the installed kernel for npm-installed
|
|
45
|
+
* users and to the source workspace for in-monorepo runs. Returns null
|
|
46
|
+
* on any I/O / parse failure (drift-detect treats null as "skip").
|
|
47
|
+
*/
|
|
48
|
+
function readInstalledKernelVersion() {
|
|
49
|
+
try {
|
|
50
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
const pkgPath = resolve(here, '..', '..', '..', 'package.json')
|
|
52
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
53
|
+
return typeof pkg?.version === 'string' ? pkg.version : null
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Read the shared update-check cache (written by the SessionStart hook
|
|
41
61
|
* via apt-check-update-worker.js) and extract the fields /apt's SKILL.md
|
|
@@ -254,7 +274,23 @@ export function cmdRoute(projectDir, extraArgs) {
|
|
|
254
274
|
const task_isolation = getTaskIsolationConfig(targetDir)
|
|
255
275
|
|
|
256
276
|
const updateCheck = readUpdateCheckCache()
|
|
257
|
-
|
|
277
|
+
let update_check = updateCheck ? { ...updateCheck } : null
|
|
278
|
+
|
|
279
|
+
// FRAMEWORK-BUG-017 rung 1: in-band detection of workspace-ahead-of-
|
|
280
|
+
// installed-kernel drift. Catches the self-host contributor case
|
|
281
|
+
// where the SessionStart worker can't see the local same-PR fix yet.
|
|
282
|
+
// Workspace wins when both signals fire (workspace is the source of
|
|
283
|
+
// truth for self-host).
|
|
284
|
+
const installedKernelVersion = readInstalledKernelVersion()
|
|
285
|
+
const drift = detectSourceWorkspaceDrift(targetDir, installedKernelVersion)
|
|
286
|
+
if (drift) {
|
|
287
|
+
update_check = {
|
|
288
|
+
update_available: true,
|
|
289
|
+
installed_version: drift.installed_version,
|
|
290
|
+
latest_version: drift.source_version,
|
|
291
|
+
reason: 'workspace source ahead of installed kernel — run /apt:update --from-source',
|
|
292
|
+
}
|
|
293
|
+
}
|
|
258
294
|
|
|
259
295
|
// ---------------- Deterministic branches (zero-behavior-change) ----------------
|
|
260
296
|
// These fire regardless of host detection status — missing host is not
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
16
16
|
import { join, resolve } from 'node:path'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getTaskIsolationConfig,
|
|
19
|
+
loadMergedProjectConfig,
|
|
20
|
+
loadProjectConfig,
|
|
21
|
+
} from '../config/load.mjs'
|
|
18
22
|
import { findMostRecentEventForTask } from '../coordination/event-log.mjs'
|
|
19
23
|
import { createCoordinationStore } from '../coordination/store.mjs'
|
|
20
24
|
import { getGitIdentity } from '../git/identity.mjs'
|
|
@@ -127,9 +131,16 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
127
131
|
// a later roadmap add-milestone/add-phase may populate it.
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
// AUDIT-001: read through the merged loader so local-only
|
|
135
|
+
// overrides for preferences.date_format / preferences.time_format
|
|
136
|
+
// win over the shared config.json fallback.
|
|
137
|
+
// Guard: malformed config.local.json must not crash task creation.
|
|
138
|
+
let config
|
|
139
|
+
try {
|
|
140
|
+
config = loadMergedProjectConfig(targetDir) ?? {}
|
|
141
|
+
} catch {
|
|
142
|
+
config = {}
|
|
143
|
+
}
|
|
133
144
|
// User-supplied --id bypasses the collision guard (caller owns the
|
|
134
145
|
// uniqueness contract). Generated ids run through a numeric-counter
|
|
135
146
|
// disambiguator that appends -2, -3, … when the candidate already
|
|
@@ -982,7 +993,50 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
982
993
|
state.last_activity = new Date().toISOString()
|
|
983
994
|
atomicWriteJson(statePath, state)
|
|
984
995
|
if (prUrlForCiWatch) {
|
|
985
|
-
|
|
996
|
+
// BUG-027 (a): pre-arm gh check. If the PR is already
|
|
997
|
+
// MERGED/CLOSED, skip arming so we don't leak a new
|
|
998
|
+
// watcher state file we'll immediately have to GC. gh
|
|
999
|
+
// rate-limit / offline / ENOENT falls back to arm (mirror
|
|
1000
|
+
// the existing rate_limited/offline patterns at lines
|
|
1001
|
+
// 1322/1326) — non-fatal so a transient gh failure
|
|
1002
|
+
// doesn't prevent legitimate watcher arming.
|
|
1003
|
+
const ghFixture = process.env.APT_GH_FIXTURE_JSON
|
|
1004
|
+
let terminalState = null
|
|
1005
|
+
if (ghFixture) {
|
|
1006
|
+
try {
|
|
1007
|
+
const parsed = JSON.parse(ghFixture)
|
|
1008
|
+
if (parsed?.state === 'MERGED' || parsed?.state === 'CLOSED') {
|
|
1009
|
+
terminalState = parsed.state
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
/* malformed fixture → fall through to arm */
|
|
1013
|
+
}
|
|
1014
|
+
} else {
|
|
1015
|
+
try {
|
|
1016
|
+
const res = spawnSync('gh', ['pr', 'view', prUrlForCiWatch, '--json', 'state'], {
|
|
1017
|
+
cwd: targetDir,
|
|
1018
|
+
encoding: 'utf-8',
|
|
1019
|
+
})
|
|
1020
|
+
if (res.status === 0 && res.stdout) {
|
|
1021
|
+
const parsed = JSON.parse(res.stdout)
|
|
1022
|
+
if (parsed?.state === 'MERGED' || parsed?.state === 'CLOSED') {
|
|
1023
|
+
terminalState = parsed.state
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/* non-zero exit / ENOENT / parse error → fall through to arm */
|
|
1027
|
+
} catch {
|
|
1028
|
+
/* never block arming on gh transport error */
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (terminalState) {
|
|
1032
|
+
ciWatchResult = {
|
|
1033
|
+
ci_watch_armed: false,
|
|
1034
|
+
ci_watch_skip_reason: 'pr-terminal-state',
|
|
1035
|
+
ci_watch: null,
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
ciWatchResult = armCiWatchAfterPrUrl(targetDir, task, prUrlForCiWatch)
|
|
1039
|
+
}
|
|
986
1040
|
}
|
|
987
1041
|
|
|
988
1042
|
regenerateTaskIndex(targetDir)
|
|
@@ -1006,6 +1060,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1006
1060
|
updateOut.ci_watch_armed = ciWatchResult.ci_watch_armed
|
|
1007
1061
|
updateOut.ci_watch_skip_reason = ciWatchResult.ci_watch_skip_reason
|
|
1008
1062
|
if (ciWatchResult.ci_watch) updateOut.ci_watch = ciWatchResult.ci_watch
|
|
1063
|
+
// FRAMEWORK-BUG-018 Track 1: surface `scheduler_handle` at the top
|
|
1064
|
+
// level so `apt-ship/SKILL.md` §7.2 can read it via
|
|
1065
|
+
// `.scheduler_handle // empty` without dotting through `.ci_watch.*`.
|
|
1066
|
+
// `null` today (no production path populates it); Track 2 lands
|
|
1067
|
+
// non-null values without re-plumbing the envelope shape.
|
|
1068
|
+
updateOut.scheduler_handle = ciWatchResult.scheduler_handle ?? null
|
|
1009
1069
|
}
|
|
1010
1070
|
updateResult = ok(updateOut)
|
|
1011
1071
|
},
|
|
@@ -1217,6 +1277,46 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1217
1277
|
// `gh pr view`, and close the ones GitHub says merged. Per spec §B1.
|
|
1218
1278
|
const onlyTaskId = flags.get('task') || null
|
|
1219
1279
|
const dryRun = flags.has('dry-run')
|
|
1280
|
+
const narrateOnly = flags.has('narrate-only')
|
|
1281
|
+
|
|
1282
|
+
// FRAMEWORK-BUG-020 — `--narrate-only` is read-only enumeration of
|
|
1283
|
+
// the pending_narration[] ledger. No `gh pr view`, no `task close`
|
|
1284
|
+
// spawn, no ledger writes. The skill body (apt-close-task §4) drains
|
|
1285
|
+
// per row via `task narration-drain` AFTER a successful narrator
|
|
1286
|
+
// spawn — at-least-once semantics is safe because the narrator is
|
|
1287
|
+
// anchor-replace idempotent (agents/apt-team-docs-narrator.md §6/§2).
|
|
1288
|
+
if (narrateOnly) {
|
|
1289
|
+
if (!existsSync(statePath)) {
|
|
1290
|
+
return ok({
|
|
1291
|
+
status: 'ok',
|
|
1292
|
+
command: 'task-close-merged',
|
|
1293
|
+
mode: 'narrate-only',
|
|
1294
|
+
closed: [],
|
|
1295
|
+
skipped: [],
|
|
1296
|
+
errors: [],
|
|
1297
|
+
})
|
|
1298
|
+
}
|
|
1299
|
+
const stateBlob = JSON.parse(readFileSync(statePath, 'utf-8'))
|
|
1300
|
+
const ledger = Array.isArray(stateBlob.pending_narration) ? stateBlob.pending_narration : []
|
|
1301
|
+
const filtered = onlyTaskId
|
|
1302
|
+
? ledger.filter((row) => row && row.task_id === onlyTaskId)
|
|
1303
|
+
: ledger
|
|
1304
|
+
return ok({
|
|
1305
|
+
status: 'ok',
|
|
1306
|
+
command: 'task-close-merged',
|
|
1307
|
+
mode: 'narrate-only',
|
|
1308
|
+
closed: filtered.map((row) => ({
|
|
1309
|
+
task_id: row.task_id,
|
|
1310
|
+
scope: row.scope ?? null,
|
|
1311
|
+
phase_id: row.phase_id ?? null,
|
|
1312
|
+
closed_at: row.closed_at ?? null,
|
|
1313
|
+
pr_number: row.pr_number ?? null,
|
|
1314
|
+
narration_pending: true,
|
|
1315
|
+
})),
|
|
1316
|
+
skipped: [],
|
|
1317
|
+
errors: [],
|
|
1318
|
+
})
|
|
1319
|
+
}
|
|
1220
1320
|
|
|
1221
1321
|
if (!existsSync(statePath)) {
|
|
1222
1322
|
return ok({
|
|
@@ -1433,6 +1533,30 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1433
1533
|
mergedAt: parsed.mergedAt ?? null,
|
|
1434
1534
|
})
|
|
1435
1535
|
|
|
1536
|
+
// BUG-027 (b): tear down any armed ci-watch state for this PR.
|
|
1537
|
+
// `doStop` is idempotent (returns `not-found` for missing state
|
|
1538
|
+
// files), so calling unconditionally here is safe. Without this
|
|
1539
|
+
// the watcher state file leaks past PR-terminal-state and
|
|
1540
|
+
// downstream operations (ci-watch sweep, health-check) flag it
|
|
1541
|
+
// as orphaned. Failure is logged into errors[] as a warning;
|
|
1542
|
+
// the merge close itself has already succeeded.
|
|
1543
|
+
try {
|
|
1544
|
+
const prNumber = parsed.number ?? extractPrNumber(task.pr_url)
|
|
1545
|
+
if (prNumber !== null) {
|
|
1546
|
+
cmdCiWatch('stop', targetDir, [
|
|
1547
|
+
'--pr',
|
|
1548
|
+
String(prNumber),
|
|
1549
|
+
'--reason',
|
|
1550
|
+
'task-closed-merged',
|
|
1551
|
+
])
|
|
1552
|
+
}
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
errors.push({
|
|
1555
|
+
id,
|
|
1556
|
+
reason: `ci-watch-stop-failed: ${e?.message ?? String(e)}`,
|
|
1557
|
+
})
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1436
1560
|
// Append a pending_narration[] ledger entry on successful merged close
|
|
1437
1561
|
// so /apt:close-task --narrate-only can drain it later. The ledger is
|
|
1438
1562
|
// additive-only; failure to write must never undo the close above
|
|
@@ -1675,6 +1799,7 @@ function armCiWatchAfterPrUrl(targetDir, task, prUrl) {
|
|
|
1675
1799
|
ci_watch_armed: true,
|
|
1676
1800
|
ci_watch_skip_reason: null,
|
|
1677
1801
|
ci_watch: envelope,
|
|
1802
|
+
scheduler_handle: envelope.scheduler_handle ?? null,
|
|
1678
1803
|
}
|
|
1679
1804
|
}
|
|
1680
1805
|
if (envelope.status === 'busy' && envelope.reason === 'already-armed') {
|
|
@@ -1682,12 +1807,14 @@ function armCiWatchAfterPrUrl(targetDir, task, prUrl) {
|
|
|
1682
1807
|
ci_watch_armed: true,
|
|
1683
1808
|
ci_watch_skip_reason: 'already-armed',
|
|
1684
1809
|
ci_watch: envelope,
|
|
1810
|
+
scheduler_handle: envelope.scheduler_handle ?? null,
|
|
1685
1811
|
}
|
|
1686
1812
|
}
|
|
1687
1813
|
return {
|
|
1688
1814
|
ci_watch_armed: false,
|
|
1689
1815
|
ci_watch_skip_reason: envelope.reason || 'start-failed',
|
|
1690
1816
|
ci_watch: envelope,
|
|
1817
|
+
scheduler_handle: envelope.scheduler_handle ?? null,
|
|
1691
1818
|
}
|
|
1692
1819
|
}
|
|
1693
1820
|
|
|
@@ -166,7 +166,12 @@ export function isAnnotationLine(line, sourceWindow) {
|
|
|
166
166
|
*/
|
|
167
167
|
export function parseFindingsMarkdown(markdown) {
|
|
168
168
|
const findings = []
|
|
169
|
-
|
|
169
|
+
// BUG-032: widen to accept H2|H3, optional `Finding:` prefix, and any of
|
|
170
|
+
// ASCII `--`, em-dash `—`, or en-dash `–` as the id/title separator. The
|
|
171
|
+
// validator-agent's consolidated `validated.md` shape uses H3 + em-dash
|
|
172
|
+
// with no `Finding:` prefix; the specialist shape uses H2 + `Finding:` +
|
|
173
|
+
// ASCII `--`. Both must round-trip through the same parser.
|
|
174
|
+
const headerRx = /^#{2,3}\s+(?:Finding:\s*)?([A-Z]+-\d+)\s*(?:--|—|–)\s*(.+)$/gm
|
|
170
175
|
const matches = [...markdown.matchAll(headerRx)]
|
|
171
176
|
for (let i = 0; i < matches.length; i++) {
|
|
172
177
|
const m = matches[i]
|
|
@@ -202,23 +207,45 @@ function parseFindingBlock(body, id, title, start, end) {
|
|
|
202
207
|
// FRAMEWORK-BUG-001 surfaced by /apt:pr-review on PR #68 in 2026-04).
|
|
203
208
|
// `(?:-\d+)?` consumes the trailing range so it doesn't leak into the
|
|
204
209
|
// file capture.
|
|
210
|
+
// BUG-032: the validator-agent's `validated.md` carries the cited path
|
|
211
|
+
// in `- **Original**:` instead of `- **File**:`. Map either spelling
|
|
212
|
+
// onto `file`/`line`; specialist shape continues to write to `file`.
|
|
213
|
+
const fileField = fields.file ?? fields.original
|
|
205
214
|
let file = null
|
|
206
215
|
let line = null
|
|
207
|
-
|
|
208
|
-
|
|
216
|
+
let lineRangeFromField = null
|
|
217
|
+
if (fileField) {
|
|
218
|
+
const fileMatch = /^`?([^`:]+?)(?::(\d+)(?:-(\d+))?)?`?\s*$/.exec(fileField)
|
|
209
219
|
if (fileMatch) {
|
|
210
220
|
file = fileMatch[1].trim()
|
|
211
221
|
line = fileMatch[2] ? Number(fileMatch[2]) : null
|
|
222
|
+
if (fileMatch[2] && fileMatch[3]) {
|
|
223
|
+
lineRangeFromField = [Number(fileMatch[2]), Number(fileMatch[3])]
|
|
224
|
+
}
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
|
215
228
|
// Description block: everything between `### Description` and the next `###`.
|
|
216
229
|
const description = extractSection(body, 'Description') ?? ''
|
|
217
230
|
|
|
218
|
-
// Evidence code block: first triple-backticked block after `### Evidence
|
|
231
|
+
// Evidence code block: first triple-backticked block after `### Evidence`
|
|
232
|
+
// (specialist shape). BUG-032: validator-consolidated `validated.md`
|
|
233
|
+
// shape carries evidence as a `- **Evidence**:` field whose value is the
|
|
234
|
+
// fenced block immediately following the field line. Fall back to the
|
|
235
|
+
// first fenced block after that field marker when the section heading
|
|
236
|
+
// form yields nothing.
|
|
219
237
|
const evidenceSection = extractSection(body, 'Evidence') ?? ''
|
|
238
|
+
let evidence
|
|
220
239
|
const fenced = /```[^\n]*\n([\s\S]*?)```/.exec(evidenceSection)
|
|
221
|
-
|
|
240
|
+
if (fenced) {
|
|
241
|
+
evidence = fenced[1]
|
|
242
|
+
} else if (evidenceSection) {
|
|
243
|
+
evidence = evidenceSection
|
|
244
|
+
} else {
|
|
245
|
+
const fieldEvidenceRx = /-\s*\*\*Evidence\*\*:\s*(?:\n+)```[^\n]*\n([\s\S]*?)```/i
|
|
246
|
+
const fieldFenced = fieldEvidenceRx.exec(body)
|
|
247
|
+
evidence = fieldFenced ? fieldFenced[1] : (fields.evidence ?? '')
|
|
248
|
+
}
|
|
222
249
|
|
|
223
250
|
// Suggested Fix section (prose + optional fenced snippets). Unlike
|
|
224
251
|
// Evidence we keep the whole section — snippets AND surrounding
|
|
@@ -231,9 +258,11 @@ function parseFindingBlock(body, id, title, start, end) {
|
|
|
231
258
|
const rangeMatch = /Lines? examined[^\d]*(\d+)\s*-\s*(\d+)/i.exec(contextSection)
|
|
232
259
|
const lineRange = rangeMatch
|
|
233
260
|
? [Number(rangeMatch[1]), Number(rangeMatch[2])]
|
|
234
|
-
:
|
|
235
|
-
?
|
|
236
|
-
:
|
|
261
|
+
: lineRangeFromField
|
|
262
|
+
? lineRangeFromField
|
|
263
|
+
: line
|
|
264
|
+
? [line, line]
|
|
265
|
+
: null
|
|
237
266
|
|
|
238
267
|
// Cross-file evidence (optional): `### Cross-File Evidence` block.
|
|
239
268
|
// Each file gets a `- **File**: path` + fenced snippet.
|
|
@@ -294,11 +323,64 @@ export function detectReferencedFiles(finding) {
|
|
|
294
323
|
return [...tokens].sort()
|
|
295
324
|
}
|
|
296
325
|
|
|
326
|
+
/**
|
|
327
|
+
* BUG-014 fuzzy-evidence helpers — token-set Jaccard + line-anchor count.
|
|
328
|
+
*
|
|
329
|
+
* Tokenize on word characters so whitespace, punctuation, and operator
|
|
330
|
+
* spacing don't perturb similarity. Same tokenizer runs on both sides so
|
|
331
|
+
* the ratio is symmetric.
|
|
332
|
+
*/
|
|
333
|
+
export function tokenizeForJaccard(text) {
|
|
334
|
+
if (!text) return []
|
|
335
|
+
return (
|
|
336
|
+
String(text)
|
|
337
|
+
.toLowerCase()
|
|
338
|
+
.match(/[a-z0-9_$]+/g) ?? []
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function jaccardSimilarity(a, b) {
|
|
343
|
+
const setA = new Set(tokenizeForJaccard(a))
|
|
344
|
+
const setB = new Set(tokenizeForJaccard(b))
|
|
345
|
+
if (setA.size === 0 && setB.size === 0) return 1
|
|
346
|
+
if (setA.size === 0 || setB.size === 0) return 0
|
|
347
|
+
let intersection = 0
|
|
348
|
+
for (const t of setA) if (setB.has(t)) intersection++
|
|
349
|
+
const union = setA.size + setB.size - intersection
|
|
350
|
+
return intersection / union
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Count evidence lines that appear as literal substrings of the cited
|
|
355
|
+
* file range (after `normalizeForLiteralMatch`). Used as the
|
|
356
|
+
* anchor-count mitigation against token-stuffing under fuzzy mode.
|
|
357
|
+
*/
|
|
358
|
+
export function countExactLineAnchors(evidenceLines, target) {
|
|
359
|
+
const normTarget = normalizeForLiteralMatch(target)
|
|
360
|
+
let count = 0
|
|
361
|
+
for (const line of evidenceLines) {
|
|
362
|
+
const normLine = normalizeForLiteralMatch(line)
|
|
363
|
+
if (!normLine) continue
|
|
364
|
+
if (normTarget.includes(normLine)) count++
|
|
365
|
+
}
|
|
366
|
+
return count
|
|
367
|
+
}
|
|
368
|
+
|
|
297
369
|
/**
|
|
298
370
|
* Check a single finding against the literal-substring rule. Returns
|
|
299
371
|
* a decision object; caller applies it to the markdown.
|
|
372
|
+
*
|
|
373
|
+
* Optional `opts.allowApprox` enables BUG-014 opt-in fuzzy fallback —
|
|
374
|
+
* when the literal check fails, compute Jaccard ≥ `approxFloor` on the
|
|
375
|
+
* cited range AND require ≥2 exact line anchors in the cited range
|
|
376
|
+
* (token-stuffing mitigation). On success, emit downgraded status
|
|
377
|
+
* `dismissed_evidence_approx_confirmed` unless `opts.llmValidatorConfirmed`
|
|
378
|
+
* is also set, in which case status becomes `confirmed_valid_approx`.
|
|
300
379
|
*/
|
|
301
|
-
export function checkFinding(finding, worktreeRoot) {
|
|
380
|
+
export function checkFinding(finding, worktreeRoot, opts = {}) {
|
|
381
|
+
const allowApprox = Boolean(opts.allowApprox)
|
|
382
|
+
const approxFloor = typeof opts.approxFloor === 'number' ? opts.approxFloor : 0.85
|
|
383
|
+
const llmValidatorConfirmed = Boolean(opts.llmValidatorConfirmed)
|
|
302
384
|
// Rule 1: evidence must be a literal substring of the cited file range.
|
|
303
385
|
if (!finding.file) {
|
|
304
386
|
return {
|
|
@@ -361,6 +443,10 @@ export function checkFinding(finding, worktreeRoot) {
|
|
|
361
443
|
const normTarget = normalizeForLiteralMatch(target)
|
|
362
444
|
const normWholeEvidence = normalizeForLiteralMatch(keptLines.join('\n'))
|
|
363
445
|
|
|
446
|
+
// Capture the first literal-mismatch reason so the fuzzy fallback can
|
|
447
|
+
// reuse it as the rejection note when fuzzy ALSO fails.
|
|
448
|
+
let literalRejectNote = null
|
|
449
|
+
|
|
364
450
|
if (normTarget.includes(normWholeEvidence)) {
|
|
365
451
|
// Fast path: the kept evidence (joined back) is still a literal
|
|
366
452
|
// contiguous substring of the cited range. This is the common
|
|
@@ -370,17 +456,43 @@ export function checkFinding(finding, worktreeRoot) {
|
|
|
370
456
|
// (e.g. `code line A\n// ... 10 more\ncode line B`). Match each
|
|
371
457
|
// kept line individually against the cited range. Every kept
|
|
372
458
|
// line must appear; any miss is a paraphrase / fabrication and
|
|
373
|
-
// the whole finding is rejected
|
|
459
|
+
// the whole finding is rejected — unless the caller opted into
|
|
460
|
+
// fuzzy fallback (BUG-014).
|
|
374
461
|
for (const line of keptLines) {
|
|
375
462
|
const normLine = normalizeForLiteralMatch(line)
|
|
376
463
|
if (!normLine) continue
|
|
377
464
|
if (!normTarget.includes(normLine)) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
465
|
+
literalRejectNote = `Evidence not found as literal substring of ${finding.file} lines ${safeStart}-${safeEnd}: line "${normLine.slice(0, 120)}" missing from cited range.`
|
|
466
|
+
break
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// BUG-014: opt-in fuzzy fallback. Default behaviour (allowApprox=false)
|
|
472
|
+
// preserves the anti-fabrication guarantee — literal mismatch returns
|
|
473
|
+
// `dismissed_evidence_not_literal` unchanged. When the caller passes
|
|
474
|
+
// `--allow-approx-evidence`, attempt Jaccard ≥ floor AND ≥2 exact
|
|
475
|
+
// line anchors in the cited range (mitigates token-stuffing). On
|
|
476
|
+
// success, downgrade status to `dismissed_evidence_approx_confirmed`
|
|
477
|
+
// unless the caller also passed `--llm-validator-confirmed`, in
|
|
478
|
+
// which case promote to `confirmed_valid_approx`.
|
|
479
|
+
if (literalRejectNote) {
|
|
480
|
+
if (!allowApprox) {
|
|
481
|
+
return { status: 'dismissed_evidence_not_literal', note: literalRejectNote }
|
|
482
|
+
}
|
|
483
|
+
const similarity = jaccardSimilarity(keptLines.join('\n'), target)
|
|
484
|
+
const anchors = countExactLineAnchors(keptLines, target)
|
|
485
|
+
if (similarity < approxFloor || anchors < 2) {
|
|
486
|
+
return {
|
|
487
|
+
status: 'dismissed_evidence_not_literal',
|
|
488
|
+
note: `${literalRejectNote} Fuzzy fallback also rejected (jaccard=${similarity.toFixed(3)} floor=${approxFloor}, anchors=${anchors}/2).`,
|
|
382
489
|
}
|
|
383
490
|
}
|
|
491
|
+
const approxNote = `Approximate match accepted under --allow-approx-evidence (jaccard=${similarity.toFixed(3)} ≥ ${approxFloor}, anchors=${anchors} ≥ 2).`
|
|
492
|
+
if (llmValidatorConfirmed) {
|
|
493
|
+
return { status: 'confirmed_valid_approx', note: approxNote }
|
|
494
|
+
}
|
|
495
|
+
return { status: 'dismissed_evidence_approx_confirmed', note: approxNote }
|
|
384
496
|
}
|
|
385
497
|
|
|
386
498
|
// Rule 2: multi-file references require Cross-File Evidence.
|
|
@@ -490,6 +602,18 @@ export function cmdValidateEvidence(findingsDir, extraArgs) {
|
|
|
490
602
|
const worktreeRoot = resolve(worktree)
|
|
491
603
|
if (!existsSync(worktreeRoot)) return err(`Worktree not found: ${worktreeRoot}`)
|
|
492
604
|
|
|
605
|
+
// BUG-014: opt-in fuzzy-evidence flags. Defaults preserve the
|
|
606
|
+
// anti-fabrication guarantee — literal substring is still the only
|
|
607
|
+
// matcher when no flag is set.
|
|
608
|
+
const allowApprox = flags.get('allow-approx-evidence') === true
|
|
609
|
+
const approxFloorRaw = flags.get('approx-floor')
|
|
610
|
+
const approxFloor = typeof approxFloorRaw === 'string' ? Number(approxFloorRaw) : 0.85
|
|
611
|
+
if (allowApprox && (!Number.isFinite(approxFloor) || approxFloor <= 0 || approxFloor > 1)) {
|
|
612
|
+
return err(`Invalid --approx-floor value: ${approxFloorRaw} (expected 0 < n ≤ 1)`)
|
|
613
|
+
}
|
|
614
|
+
const llmValidatorConfirmed = flags.get('llm-validator-confirmed') === true
|
|
615
|
+
const checkOpts = { allowApprox, approxFloor, llmValidatorConfirmed }
|
|
616
|
+
|
|
493
617
|
const files = readdirSync(dir).filter((f) => f.endsWith('.md'))
|
|
494
618
|
const summary = {
|
|
495
619
|
status: 'ok',
|
|
@@ -506,13 +630,21 @@ export function cmdValidateEvidence(findingsDir, extraArgs) {
|
|
|
506
630
|
const full = join(dir, f)
|
|
507
631
|
const md = readFileSync(full, 'utf-8')
|
|
508
632
|
const findings = parseFindingsMarkdown(md)
|
|
633
|
+
|
|
634
|
+
// BUG-032: always record the file in `processed_files`. The
|
|
635
|
+
// previous `continue` on empty findings caused validated.md to
|
|
636
|
+
// silently no-op when the header regex didn't match, leaving
|
|
637
|
+
// downstream gates with `processed_files: []` and no audit trail
|
|
638
|
+
// that the file was even read.
|
|
639
|
+
summary.processed_files.push({ file: f, findings_count: findings.length })
|
|
640
|
+
|
|
509
641
|
if (findings.length === 0) continue
|
|
510
642
|
|
|
511
643
|
const decisions = []
|
|
512
644
|
for (const finding of findings) {
|
|
513
|
-
const decision = checkFinding(finding, worktreeRoot)
|
|
645
|
+
const decision = checkFinding(finding, worktreeRoot, checkOpts)
|
|
514
646
|
decisions.push({ finding, decision })
|
|
515
|
-
if (decision.status === 'confirmed_valid') {
|
|
647
|
+
if (decision.status === 'confirmed_valid' || decision.status === 'confirmed_valid_approx') {
|
|
516
648
|
summary.validated++
|
|
517
649
|
} else {
|
|
518
650
|
summary.rejected++
|
|
@@ -530,7 +662,16 @@ export function cmdValidateEvidence(findingsDir, extraArgs) {
|
|
|
530
662
|
if (rewritten !== md) {
|
|
531
663
|
writeFileSync(full, rewritten, 'utf-8')
|
|
532
664
|
}
|
|
533
|
-
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// BUG-032: surface parser-shape drift early. If we read N markdown
|
|
668
|
+
// files but extracted zero findings, warn on stderr so future schema
|
|
669
|
+
// changes don't silently no-op the gate.
|
|
670
|
+
const totalFindings = summary.processed_files.reduce((acc, e) => acc + (e.findings_count ?? 0), 0)
|
|
671
|
+
if (files.length > 0 && totalFindings === 0) {
|
|
672
|
+
process.stderr.write(
|
|
673
|
+
`WARNING: validate-evidence read ${files.length} markdown file(s) but extracted 0 findings — parser shape mismatch?\n`,
|
|
674
|
+
)
|
|
534
675
|
}
|
|
535
676
|
|
|
536
677
|
return ok(summary)
|