session-orchestrator 3.20.0 → 3.22.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -16,6 +16,40 @@
|
|
|
16
16
|
import { parseStateMd, serializeStateMd } from './yaml-parser.mjs';
|
|
17
17
|
import { updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mjs';
|
|
18
18
|
|
|
19
|
+
const MISSION_STATUS_HEADING_RE = /^##\s+Mission Status\s*$/;
|
|
20
|
+
const WRITER_TIMESTAMP_SOURCE = '\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z';
|
|
21
|
+
const WRITER_TIMESTAMP_RE = new RegExp(`^${WRITER_TIMESTAMP_SOURCE}$`);
|
|
22
|
+
const CANONICAL_MISSION_STATUS_ENTRY_RE = new RegExp(
|
|
23
|
+
`^- ([a-z][a-z0-9]*(?:-[a-z0-9]+)*-\\d+): (.*) \\(updated (${WRITER_TIMESTAMP_SOURCE})\\)$`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Finds the first exact `## Mission Status` section and its closing heading.
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} lines
|
|
30
|
+
* @returns {{ headingIdx: number, sectionEnd: number }|null}
|
|
31
|
+
*/
|
|
32
|
+
function findMissionStatusSection(lines) {
|
|
33
|
+
if (!Array.isArray(lines)) return null;
|
|
34
|
+
const headingIdx = lines.findIndex((line) => MISSION_STATUS_HEADING_RE.test(line));
|
|
35
|
+
if (headingIdx === -1) return null;
|
|
36
|
+
|
|
37
|
+
let sectionEnd = lines.length;
|
|
38
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
39
|
+
if (/^##\s+/.test(lines[i])) {
|
|
40
|
+
sectionEnd = i;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { headingIdx, sectionEnd };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isWriterTimestamp(timestamp) {
|
|
48
|
+
if (!WRITER_TIMESTAMP_RE.test(timestamp)) return false;
|
|
49
|
+
const parsed = new Date(timestamp);
|
|
50
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === timestamp;
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
/**
|
|
20
54
|
* Parses the optional `mission-status:` block from a STATE.md frontmatter object
|
|
21
55
|
* (as returned by `parseStateMd(...).frontmatter`).
|
|
@@ -24,8 +58,9 @@ import { updateFrontmatterFields, writeStateMd } from './frontmatter-mutators.mj
|
|
|
24
58
|
* STATE.md files). Returns `[]` when the key is present but the value is an empty
|
|
25
59
|
* array. Returns the array of entries when present and non-empty.
|
|
26
60
|
*
|
|
27
|
-
* Does NOT validate individual entry shapes
|
|
28
|
-
*
|
|
61
|
+
* Does NOT validate individual entry shapes, and no helper in this repo does:
|
|
62
|
+
* entry shape and the `status` enum are coordinator convention, deliberately not a
|
|
63
|
+
* mechanical gate (see `syncFrontmatterMissionStatus` below for why).
|
|
29
64
|
*
|
|
30
65
|
* @param {object} frontmatter
|
|
31
66
|
* @returns {object[]|null}
|
|
@@ -55,9 +90,9 @@ export function parseMissionStatus(frontmatter) {
|
|
|
55
90
|
* - Works on string input (pure — no file I/O). Returns the updated STATE.md contents.
|
|
56
91
|
* - No-ops if `contents` has no parseable frontmatter (returns input unchanged).
|
|
57
92
|
*
|
|
58
|
-
* Individual entry objects
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* Individual entry objects are expected to carry `{ id, task, wave, status }`, but
|
|
94
|
+
* this function does NOT enforce that shape and no validator in this repo does —
|
|
95
|
+
* callers own the invariant.
|
|
61
96
|
*
|
|
62
97
|
* @param {string} contents
|
|
63
98
|
* @param {object[]|null|undefined} missionStatusArray
|
|
@@ -82,19 +117,31 @@ export function writeMissionStatus(contents, missionStatusArray) {
|
|
|
82
117
|
* entry level — the input is never mutated, which keeps `parseMissionStatus`'s
|
|
83
118
|
* shallow-copy contract intact for anything else holding the same nested entries).
|
|
84
119
|
*
|
|
85
|
-
* UPDATE-ONLY by design: when the key is absent, is not an array, or holds no
|
|
86
|
-
*
|
|
87
|
-
* neither created nor an error, because `setMissionStatus(contents,
|
|
88
|
-
* knows only `id` and `status` — it lacks the `task` and `wave`
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
120
|
+
* UPDATE-ONLY by design: when the key is absent, is not an array, or holds no
|
|
121
|
+
* matching entry in a populated array, the frontmatter is returned unchanged. It is
|
|
122
|
+
* deliberately neither created nor an error, because `setMissionStatus(contents,
|
|
123
|
+
* taskId, status)` knows only `id` and `status` — it lacks the `task` and `wave`
|
|
124
|
+
* fields a full entry carries, so a synthesised entry would be shape-invalid yet look
|
|
125
|
+
* authoritative to frontmatter consumers such as `vault-status/narrative-mirror.mjs`.
|
|
126
|
+
* Throwing is likewise excluded by the never-throw contract of `setMissionStatus`.
|
|
127
|
+
*
|
|
128
|
+
* An empty array is recovered from the final body by
|
|
129
|
+
* `recoverFrontmatterMissionStatus`, which persists only the truthful `id` and
|
|
130
|
+
* `status` values available there. It does not fabricate absent metadata.
|
|
93
131
|
*
|
|
94
132
|
* `status` is mirrored verbatim without an enum check on purpose: gating it would
|
|
95
133
|
* reintroduce the exact divergence (body says X, frontmatter says Y) this sync exists
|
|
96
|
-
* to remove. An out-of-enum value
|
|
97
|
-
*
|
|
134
|
+
* to remove. An out-of-enum value therefore lands visibly on BOTH surfaces rather than
|
|
135
|
+
* being silently rejected on one.
|
|
136
|
+
*
|
|
137
|
+
* There is deliberately NO transition validator behind this. A `mission-status-schema.mjs`
|
|
138
|
+
* once existed (#340) offering `isValidMissionStatusTransition`; it was never wired and
|
|
139
|
+
* was removed, because the only guard form it enabled — read the current status, reject a
|
|
140
|
+
* disallowed transition — would reject legitimate live writes. Measured against a copy of
|
|
141
|
+
* this repo's own STATE.md carrying 24 items: 12 had no body entry yet (current status
|
|
142
|
+
* reads `null`, so every transition out of them is "invalid"), and the routine
|
|
143
|
+
* `in-dev` → `completed` write is not in the strict forward chain either — 18 of 24
|
|
144
|
+
* writes would have been refused. The enum is a vocabulary, not a state machine.
|
|
98
145
|
*
|
|
99
146
|
* @param {object} frontmatter
|
|
100
147
|
* @param {string} taskId
|
|
@@ -116,6 +163,80 @@ function syncFrontmatterMissionStatus(frontmatter, taskId, status) {
|
|
|
116
163
|
return { ...frontmatter, 'mission-status': entries };
|
|
117
164
|
}
|
|
118
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Mirrors body-only task IDs into the frontmatter `mission-status` registry as
|
|
168
|
+
* partial `{ id, status }` entries. This is a SUPERSET merge: existing entries are
|
|
169
|
+
* never rewritten or reordered, so their full metadata (`task`, `wave`) survives —
|
|
170
|
+
* only IDs absent from the registry are appended.
|
|
171
|
+
*
|
|
172
|
+
* It must not be gated on an EMPTY registry. Recovering only from empty froze the
|
|
173
|
+
* registry after its first recovery: the sync path (`syncFrontmatterMissionStatus`)
|
|
174
|
+
* is update-only, so every subsequently added task ID was never mirrored again, and
|
|
175
|
+
* Phase 1.9/1.10 read a plausible undercount instead of the obvious zero. That is
|
|
176
|
+
* #1084 one write later — measured m-1/m-2/m-3 in the body against `[m-1]` in the
|
|
177
|
+
* frontmatter.
|
|
178
|
+
*
|
|
179
|
+
* The section parse stays strict and all-or-nothing: any nonblank line that is not a
|
|
180
|
+
* unique canonical writer bullet (unsafe ID, pipe, malformed timestamp, duplicate,
|
|
181
|
+
* prose) aborts the whole merge rather than fabricating entries from an ambiguous
|
|
182
|
+
* body. Known limitation: one foreign-but-writer-accepted ID (e.g. `Docs_2`) or a
|
|
183
|
+
* hand-written legacy bullet without a timestamp therefore suppresses the merge for
|
|
184
|
+
* the whole file, silently.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} frontmatter
|
|
187
|
+
* @param {string} body
|
|
188
|
+
* @returns {object}
|
|
189
|
+
*/
|
|
190
|
+
function recoverFrontmatterMissionStatus(frontmatter, body) {
|
|
191
|
+
if (frontmatter === null || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
|
|
192
|
+
return frontmatter;
|
|
193
|
+
}
|
|
194
|
+
const raw = frontmatter['mission-status'];
|
|
195
|
+
if (!Array.isArray(raw) || typeof body !== 'string') return frontmatter;
|
|
196
|
+
|
|
197
|
+
const lines = body.split('\n');
|
|
198
|
+
const section = findMissionStatusSection(lines);
|
|
199
|
+
if (section === null) return frontmatter;
|
|
200
|
+
|
|
201
|
+
const entries = [];
|
|
202
|
+
const ids = new Set();
|
|
203
|
+
for (let i = section.headingIdx + 1; i < section.sectionEnd; i++) {
|
|
204
|
+
const line = lines[i];
|
|
205
|
+
if (line.trim() === '') continue;
|
|
206
|
+
|
|
207
|
+
const match = CANONICAL_MISSION_STATUS_ENTRY_RE.exec(line);
|
|
208
|
+
if (match === null) return frontmatter;
|
|
209
|
+
const [, id, status, timestamp] = match;
|
|
210
|
+
if (
|
|
211
|
+
status.length === 0 ||
|
|
212
|
+
status.includes('|') ||
|
|
213
|
+
status.includes('\n') ||
|
|
214
|
+
!isWriterTimestamp(timestamp) ||
|
|
215
|
+
ids.has(id)
|
|
216
|
+
) {
|
|
217
|
+
return frontmatter;
|
|
218
|
+
}
|
|
219
|
+
ids.add(id);
|
|
220
|
+
entries.push({ id, status });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const known = new Set(
|
|
224
|
+
raw
|
|
225
|
+
.filter((e) => e !== null && typeof e === 'object' && !Array.isArray(e))
|
|
226
|
+
.map((e) => e.id)
|
|
227
|
+
);
|
|
228
|
+
const added = entries.filter((e) => !known.has(e.id));
|
|
229
|
+
if (added.length === 0) return frontmatter;
|
|
230
|
+
return { ...frontmatter, 'mission-status': [...raw, ...added] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function serializeMissionStatusUpdate(frontmatter, body) {
|
|
234
|
+
return serializeStateMd({
|
|
235
|
+
frontmatter: recoverFrontmatterMissionStatus(frontmatter, body),
|
|
236
|
+
body,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
119
240
|
/**
|
|
120
241
|
* Sets (or updates) the mission status for a single task in the `## Mission Status` body
|
|
121
242
|
* section of STATE.md. Creates the section if it does not exist.
|
|
@@ -128,7 +249,10 @@ function syncFrontmatterMissionStatus(frontmatter, taskId, status) {
|
|
|
128
249
|
* during a wave; the frontmatter array is what `parseMissionStatus` consumers read
|
|
129
250
|
* (`vault-status/narrative-mirror.mjs`, session-end Phase 1.9/1.10). Before this sync the
|
|
130
251
|
* live writer and the reader sat on different surfaces and drifted apart in both
|
|
131
|
-
* directions.
|
|
252
|
+
* directions. A legacy empty registry is recovered from canonical body bullets as
|
|
253
|
+
* partial `{ id, status }` entries, which lets frontmatter readers classify the work
|
|
254
|
+
* without fabricated metadata. Ambiguous legacy bodies remain empty; other unmatched
|
|
255
|
+
* populated entries remain update-only. See `syncFrontmatterMissionStatus`.
|
|
132
256
|
*
|
|
133
257
|
* Pure function — no I/O. Returns original `contents` unchanged on bad input.
|
|
134
258
|
*
|
|
@@ -151,31 +275,16 @@ export function setMissionStatus(contents, taskId, status) {
|
|
|
151
275
|
const bullet = `- ${taskId}: ${status} (updated ${timestamp})`;
|
|
152
276
|
const lines = parsed.body.split('\n');
|
|
153
277
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (let i = 0; i < lines.length; i++) {
|
|
157
|
-
if (/^##\s+Mission Status\b/.test(lines[i])) {
|
|
158
|
-
headingIdx = i;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (headingIdx === -1) {
|
|
278
|
+
const section = findMissionStatusSection(lines);
|
|
279
|
+
if (section === null) {
|
|
164
280
|
// Section does not exist — append it at the end
|
|
165
281
|
let bodyOut = parsed.body;
|
|
166
282
|
if (!bodyOut.endsWith('\n')) bodyOut += '\n';
|
|
167
283
|
bodyOut += `\n## Mission Status\n\n${bullet}\n`;
|
|
168
|
-
return
|
|
284
|
+
return serializeMissionStatusUpdate(frontmatter, bodyOut);
|
|
169
285
|
}
|
|
170
286
|
|
|
171
|
-
|
|
172
|
-
let sectionEnd = lines.length;
|
|
173
|
-
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
174
|
-
if (/^##\s+/.test(lines[i])) {
|
|
175
|
-
sectionEnd = i;
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
287
|
+
const { headingIdx, sectionEnd } = section;
|
|
179
288
|
|
|
180
289
|
// Look for an existing entry with this taskId within the section
|
|
181
290
|
const entryRe = new RegExp(`^-\\s+${taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:`);
|
|
@@ -205,18 +314,20 @@ export function setMissionStatus(contents, taskId, status) {
|
|
|
205
314
|
const before = lines.slice(0, headingIdx + 1);
|
|
206
315
|
const after = lines.slice(insertAt);
|
|
207
316
|
const rebuilt = [...before, '', bullet, ...after];
|
|
208
|
-
return
|
|
317
|
+
return serializeMissionStatusUpdate(frontmatter, rebuilt.join('\n'));
|
|
209
318
|
}
|
|
210
319
|
}
|
|
211
320
|
|
|
212
|
-
return
|
|
321
|
+
return serializeMissionStatusUpdate(frontmatter, lines.join('\n'));
|
|
213
322
|
}
|
|
214
323
|
|
|
215
324
|
/**
|
|
216
325
|
* Reads the current mission status for a single task from the `## Mission Status` body
|
|
217
326
|
* section of STATE.md.
|
|
218
327
|
*
|
|
219
|
-
* Returns the status string (e.g. `'in-dev'`
|
|
328
|
+
* Returns the full status string before a current-writer timestamp (e.g. `'in-dev'`
|
|
329
|
+
* or `'needs manual testing'`). For legacy body lines that lack that exact form, falls
|
|
330
|
+
* back to the first status token. Returns `null` if the task is not found or the
|
|
220
331
|
* section does not exist. Never throws — returns `null` on any bad input.
|
|
221
332
|
*
|
|
222
333
|
* @param {string} contents - Current STATE.md file contents (string)
|
|
@@ -230,29 +341,24 @@ export function readMissionStatus(contents, taskId) {
|
|
|
230
341
|
if (parsed === null) return null;
|
|
231
342
|
|
|
232
343
|
const lines = parsed.body.split('\n');
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (/^##\s+Mission Status\b/.test(lines[i])) {
|
|
236
|
-
headingIdx = i;
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (headingIdx === -1) return null;
|
|
344
|
+
const section = findMissionStatusSection(lines);
|
|
345
|
+
if (section === null) return null;
|
|
241
346
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
347
|
+
// Prefer the full current-writer status, including internal spaces.
|
|
348
|
+
const escapedId = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
349
|
+
const writerEntryRe = new RegExp(
|
|
350
|
+
`^- ${escapedId}: (.*) \\(updated ${WRITER_TIMESTAMP_SOURCE}\\)$`
|
|
351
|
+
);
|
|
352
|
+
for (let i = section.headingIdx + 1; i < section.sectionEnd; i++) {
|
|
353
|
+
const match = writerEntryRe.exec(lines[i]);
|
|
354
|
+
if (match) return match[1];
|
|
248
355
|
}
|
|
249
356
|
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (m) return m[1];
|
|
357
|
+
// Reader-only legacy compatibility. Recovery remains canonical and all-or-nothing.
|
|
358
|
+
const legacyEntryRe = new RegExp(`^-\\s+${escapedId}:\\s+(\\S+)`);
|
|
359
|
+
for (let i = section.headingIdx + 1; i < section.sectionEnd; i++) {
|
|
360
|
+
const match = legacyEntryRe.exec(lines[i]);
|
|
361
|
+
if (match) return match[1];
|
|
256
362
|
}
|
|
257
363
|
return null;
|
|
258
364
|
}
|
|
@@ -6,20 +6,116 @@
|
|
|
6
6
|
* so all commands are wrapped in a `while true; sleep` loop per D5 findings.
|
|
7
7
|
*
|
|
8
8
|
* Issue #561 — ADR-0007 tmux-visualization substrate.
|
|
9
|
+
*
|
|
10
|
+
* ## The probed pane commands — SSOT for the lockstep test
|
|
11
|
+
*
|
|
12
|
+
* The two lines below are the argv this module emits, with the resolved repo
|
|
13
|
+
* spec written as `<spec>` and the `2>&1` redirect dropped. They are not a
|
|
14
|
+
* description of the commands: `tests/lib/tmux-layout/vcs-detector.test.mjs`
|
|
15
|
+
* reconstructs the same normalisation from `detectVcsCommand()` and asserts
|
|
16
|
+
* equality, so a flag added to the emitted command without a matching probe
|
|
17
|
+
* here turns that test RED. See § Why a probe line, not prose.
|
|
18
|
+
*
|
|
19
|
+
* PROBE 2026-08-14 glab 1.91.0: glab ci status -R <spec> --output json
|
|
20
|
+
* PROBE 2026-08-14 gh 2.86.0: gh pr checks --watch
|
|
21
|
+
*
|
|
22
|
+
* ## Why `glab ci status` carries no pipeline selector (#1022)
|
|
23
|
+
*
|
|
24
|
+
* The glab pane emitted `--pipeline-id LATEST` until 2026-08-14. That flag does
|
|
25
|
+
* not exist on any `glab ci` subcommand, and `glab ci status --help` (1.91.0)
|
|
26
|
+
* lists only `-b --branch`, `-c --compact`, `-l --live`, `-F --output`,
|
|
27
|
+
* `-R --repo`. Probed from a non-git cwd against this repo's own remote:
|
|
28
|
+
*
|
|
29
|
+
* with the flag → `ERROR Unknown flag: --pipeline-id.`, exit 1
|
|
30
|
+
* without it → exit 0, `{"jobs":[…],"pipeline":{…}}`
|
|
31
|
+
*
|
|
32
|
+
* So the flag was the sole cause and it has NO replacement: the argument-less
|
|
33
|
+
* form already means "the pipeline of the current branch" (glab's own help
|
|
34
|
+
* example). `--branch=<name>` is the only selector glab offers and is
|
|
35
|
+
* deliberately NOT used — it would freeze a render-time branch snapshot into a
|
|
36
|
+
* pane that must follow the operator's checkout, the same reason the gh pane
|
|
37
|
+
* takes no positional.
|
|
38
|
+
*
|
|
39
|
+
* The `jq` filter was dead by the same measurement and is fixed with it: the
|
|
40
|
+
* payload is an OBJECT keyed `jobs`/`pipeline`, so the old `.[]` raised
|
|
41
|
+
* `Cannot index array with string "name"` (jq exit 5) on every real response.
|
|
42
|
+
* `.jobs[]` returns one `<status> <name>` line per job. Because the whole
|
|
43
|
+
* pipeline is `… 2>&1 | jq … 2>/dev/null || echo …`, both failures were
|
|
44
|
+
* swallowed into the fallback text — the pane printed "glab not available or no
|
|
45
|
+
* pipeline" against a healthy pipeline, indistinguishable from a missing binary.
|
|
46
|
+
*
|
|
47
|
+
* ## Why only the glab pane carries `-R` (#971)
|
|
48
|
+
*
|
|
49
|
+
* The pane commands run in whatever cwd the operator pastes the one-liner into,
|
|
50
|
+
* so an omitted `-R`/`--repo` silently targets the ambient cwd remote. Both
|
|
51
|
+
* halves were probed against the installed binaries on 2026-08-14
|
|
52
|
+
* (glab 1.91.0 / gh 2.86.0) and they do NOT behave the same:
|
|
53
|
+
*
|
|
54
|
+
* - `glab ci status -R <spec> --output json` from a NON-git cwd → exit 0 with
|
|
55
|
+
* real pipeline JSON. The flag is applicable and makes the pane
|
|
56
|
+
* cwd-independent, so it is passed whenever `resolveRepoSpec()` resolves one.
|
|
57
|
+
* - `gh pr checks -R <spec>` with NO positional argument →
|
|
58
|
+
* `argument required when using the --repo flag` (exit non-zero, no output).
|
|
59
|
+
* gh selects "the PR of the current branch" ONLY in the argument-less form;
|
|
60
|
+
* `-R` is legal there only alongside a `<number> | <url> | <branch>`
|
|
61
|
+
* positional, which this pane deliberately does not have (the watched branch
|
|
62
|
+
* must follow the operator's checkout, and a render-time branch snapshot
|
|
63
|
+
* would go stale on the next `git switch`). Adding `-R` there would break the
|
|
64
|
+
* pane at runtime — the exact defect shape recorded in
|
|
65
|
+
* `scripts/lib/ci-status-banner.mjs` (`-R` on `gh repo view`, which rejects
|
|
66
|
+
* it). The gh pane therefore stays cwd-scoped BY MEASUREMENT, not by
|
|
67
|
+
* oversight.
|
|
68
|
+
*
|
|
69
|
+
* ## Why a probe line, not prose
|
|
70
|
+
*
|
|
71
|
+
* The #971 sweep added `-R` here and left `--pipeline-id` untouched, then wrote
|
|
72
|
+
* a header documenting a probe of `glab ci status -R <spec> --output json` —
|
|
73
|
+
* a command the pane never emitted. The measurement was real and cited for
|
|
74
|
+
* something it had not measured, so a dead flag rode through a green test.
|
|
75
|
+
* NAMED CEILING: the lockstep only proves that command and probe were changed
|
|
76
|
+
* together; it cannot prove the probe was re-executed. It buys the one step
|
|
77
|
+
* that was skipped — no flag change without touching the measurement record.
|
|
78
|
+
* Revisit if a pane ever needs a runtime-varying flag, which this shape cannot
|
|
79
|
+
* express.
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
import { resolveRepoSpec } from '../vcs-repo-spec.mjs';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shell-safe spec shape. A git remote URL (`git@host:group/project.git`,
|
|
86
|
+
* `https://host/group/project.git`) and an `OWNER/REPO` spec are both fully
|
|
87
|
+
* inside this character class, so a matching value can be spliced into the pane
|
|
88
|
+
* command line without quoting. A value OUTSIDE it is DROPPED (the flag is
|
|
89
|
+
* omitted) rather than quoted — this mirrors `resolveRepoSpec`'s own contract
|
|
90
|
+
* that an unresolvable spec means "omit the flag entirely", never "emit a
|
|
91
|
+
* broken one", and keeps a hostile remote URL out of the pane's shell.
|
|
9
92
|
*/
|
|
93
|
+
const SHELL_SAFE_SPEC_RE = /^[A-Za-z0-9._:/@+-]+$/;
|
|
10
94
|
|
|
11
95
|
/**
|
|
12
96
|
* Resolve the CI-watch pane command based on Session Config vcs: key.
|
|
13
97
|
*
|
|
14
|
-
* @param {{
|
|
98
|
+
* @param {{
|
|
99
|
+
* config?: object,
|
|
100
|
+
* projectRoot: string,
|
|
101
|
+
* gitRun?: (args: string[]) => { ok: boolean, stdout: string, stderr: string }
|
|
102
|
+
* }} args
|
|
103
|
+
* - gitRun: optional `resolveRepoSpec` seam, so callers/tests can pin the
|
|
104
|
+
* resolved repo spec instead of spawning `git` against the real checkout.
|
|
15
105
|
* @returns {{ bin: 'glab'|'gh'|null, command: string, fallback: string, blocking: boolean, platform: 'gitlab'|'github'|null }}
|
|
16
106
|
* - command: the shell command for Pane 3 (wrapped in poll-loop since glab/gh are poll-based, not live-tail)
|
|
17
107
|
* - fallback: shell command shown when bin is not available in PATH
|
|
18
108
|
*/
|
|
19
|
-
export function detectVcsCommand({ config, projectRoot
|
|
109
|
+
export function detectVcsCommand({ config, projectRoot, gitRun }) {
|
|
20
110
|
const vcs = config?.vcs; // 'gitlab' | 'github' | undefined
|
|
21
111
|
|
|
22
112
|
if (vcs === 'gitlab') {
|
|
113
|
+
const spec = resolveRepoSpec({
|
|
114
|
+
repoRoot: projectRoot,
|
|
115
|
+
vcs: 'gitlab',
|
|
116
|
+
...(typeof gitRun === 'function' ? { gitRun } : {}),
|
|
117
|
+
});
|
|
118
|
+
const repoFlag = spec && SHELL_SAFE_SPEC_RE.test(spec) ? ` -R ${spec}` : '';
|
|
23
119
|
return {
|
|
24
120
|
bin: 'glab',
|
|
25
121
|
platform: 'gitlab',
|
|
@@ -29,8 +125,12 @@ export function detectVcsCommand({ config, projectRoot: _projectRoot }) {
|
|
|
29
125
|
' clear;',
|
|
30
126
|
' date;',
|
|
31
127
|
" echo '--- glab ci status (refresh: 15s) ---';",
|
|
32
|
-
|
|
33
|
-
|
|
128
|
+
// No pipeline selector: `--pipeline-id` is not a glab flag (1.91.0
|
|
129
|
+
// rejects it outright) and the argument-less form already means "the
|
|
130
|
+
// current branch's pipeline". `.jobs[]`, not `.[]` — the payload is an
|
|
131
|
+
// object. Both measured 2026-08-14; see the module header.
|
|
132
|
+
` glab ci status${repoFlag} --output json 2>&1`,
|
|
133
|
+
" | jq -r '.jobs[] | \"\\(.status) \\(.name)\"' 2>/dev/null",
|
|
34
134
|
" || echo 'glab not available or no pipeline';",
|
|
35
135
|
' sleep 15;',
|
|
36
136
|
'done',
|
|
@@ -55,6 +155,10 @@ export function detectVcsCommand({ config, projectRoot: _projectRoot }) {
|
|
|
55
155
|
' clear;',
|
|
56
156
|
' date;',
|
|
57
157
|
" echo '--- gh pr checks --watch (will exit on PR completion) ---';",
|
|
158
|
+
// No `-R` here BY MEASUREMENT, not by oversight: `gh pr checks -R <spec>`
|
|
159
|
+
// without a positional PR/branch argument is rejected outright
|
|
160
|
+
// ("argument required when using the `--repo` flag", gh 2.86.0), and this
|
|
161
|
+
// pane must follow the operator's current branch. See the module header.
|
|
58
162
|
' gh pr checks --watch 2>&1',
|
|
59
163
|
" || echo 'no PR in current branch';",
|
|
60
164
|
' sleep 15;',
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
8
8
|
import { join, resolve, dirname } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
10
11
|
import { ALLOWED_MODEL_ALIASES, MODEL_ID_RE } from '../agent-frontmatter.mjs';
|
|
12
|
+
import { extractInitialFrontmatter } from './frontmatter-block.mjs';
|
|
11
13
|
|
|
12
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
|
|
@@ -57,10 +59,45 @@ function fail(msg) {
|
|
|
57
59
|
/**
|
|
58
60
|
* Extract YAML frontmatter content (between first --- and second ---).
|
|
59
61
|
* Returns null if no valid frontmatter block found.
|
|
62
|
+
*
|
|
63
|
+
* Delegates to the shared extractor. This file carried a THIRD verbatim copy of
|
|
64
|
+
* that regex until now (check-skills.mjs and check-commands.mjs held the other
|
|
65
|
+
* two, which is why the extraction was lifted into frontmatter-block.mjs). Only
|
|
66
|
+
* the block-finding is shared — the field RULES stay per-gate and disagree on
|
|
67
|
+
* purpose; see the header of frontmatter-block.mjs.
|
|
68
|
+
*
|
|
69
|
+
* Checks 7-10 only need "text, or nothing"; Check 6 reads the shared
|
|
70
|
+
* diagnostic directly for a precise message.
|
|
60
71
|
*/
|
|
61
72
|
function extractFrontmatter(content) {
|
|
62
|
-
const
|
|
63
|
-
return
|
|
73
|
+
const extracted = extractInitialFrontmatter(content);
|
|
74
|
+
return extracted.ok ? extracted.yamlText : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a frontmatter block with a real YAML parser (CORE_SCHEMA, matching
|
|
79
|
+
* check-commands.mjs and check-skills.mjs).
|
|
80
|
+
*
|
|
81
|
+
* @param {string} yamlText - the block's inner text, no fences
|
|
82
|
+
* @returns {{ ok: true, data: unknown } | { ok: false, message: string }}
|
|
83
|
+
* On failure, `message` is a caller-printable reason with a frontmatter-local
|
|
84
|
+
* line/column when js-yaml supplies one.
|
|
85
|
+
*/
|
|
86
|
+
function parseFrontmatterYaml(yamlText) {
|
|
87
|
+
let data;
|
|
88
|
+
try {
|
|
89
|
+
data = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const reason = error?.reason ?? error?.message ?? String(error);
|
|
92
|
+
const location = error?.mark
|
|
93
|
+
? ` at frontmatter line ${error.mark.line + 1}, column ${error.mark.column + 1}`
|
|
94
|
+
: '';
|
|
95
|
+
return { ok: false, message: `invalid YAML frontmatter: ${reason}${location}` };
|
|
96
|
+
}
|
|
97
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
98
|
+
return { ok: false, message: 'YAML frontmatter must be a non-null mapping/object' };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, data };
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
/**
|
|
@@ -146,17 +183,48 @@ const mdFiles = readdirSync(agentsDir).filter(isAgentDefFile);
|
|
|
146
183
|
if (mdFiles.length === 0) {
|
|
147
184
|
fail('agents directory is empty (no .md files)');
|
|
148
185
|
} else {
|
|
186
|
+
let parseable = 0;
|
|
187
|
+
|
|
149
188
|
for (const agentFile of mdFiles) {
|
|
150
189
|
const agentName = agentFile;
|
|
151
190
|
const filePath = join(agentsDir, agentFile);
|
|
152
191
|
const content = readFileSync(filePath, 'utf8');
|
|
153
192
|
|
|
154
|
-
const
|
|
193
|
+
const extracted = extractInitialFrontmatter(content);
|
|
155
194
|
|
|
156
|
-
if (!
|
|
157
|
-
fail(`${agentName}:
|
|
195
|
+
if (!extracted.ok) {
|
|
196
|
+
fail(`${agentName}: ${extracted.diagnostic}`);
|
|
158
197
|
continue;
|
|
159
198
|
}
|
|
199
|
+
const frontmatter = extracted.yamlText;
|
|
200
|
+
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
// The block is valid YAML.
|
|
203
|
+
//
|
|
204
|
+
// Every rule below reads the block with LINE REGEXES (getField/hasField),
|
|
205
|
+
// which model Claude Code's own lenient loader. That leniency is exactly
|
|
206
|
+
// how 14 of 16 agent definitions sat unparseable in the tree unnoticed: an
|
|
207
|
+
// unquoted single-line `description:` containing a `: ` (e.g.
|
|
208
|
+
// `<example>Context: /plan ...`) is not YAML, and a regex looking only for
|
|
209
|
+
// `^description:` sees nothing wrong. A ` #` inside such a value is worse
|
|
210
|
+
// still — it opens a YAML comment, so the block PARSES while silently
|
|
211
|
+
// truncating the description at that point.
|
|
212
|
+
//
|
|
213
|
+
// A parse failure is reported alone (no fall-through to the field rules):
|
|
214
|
+
// the block is broken at the root, and six regex verdicts stacked on top
|
|
215
|
+
// of it are cascade noise that buries the one defect worth fixing. Same
|
|
216
|
+
// posture as check-skills.mjs R8.
|
|
217
|
+
//
|
|
218
|
+
// ORTHOGONAL to the `description` block-scalar ban further down: this rule
|
|
219
|
+
// demands the block BE YAML; that one forbids one particular valid YAML
|
|
220
|
+
// form the agent loader cannot read. Neither implies the other.
|
|
221
|
+
// ------------------------------------------------------------------
|
|
222
|
+
const parsedFm = parseFrontmatterYaml(frontmatter);
|
|
223
|
+
if (!parsedFm.ok) {
|
|
224
|
+
fail(`${agentName}: ${parsedFm.message}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
parseable++;
|
|
160
228
|
|
|
161
229
|
// ------------------------------------------------------------------
|
|
162
230
|
// Required fields check
|
|
@@ -236,6 +304,10 @@ if (mdFiles.length === 0) {
|
|
|
236
304
|
}
|
|
237
305
|
}
|
|
238
306
|
}
|
|
307
|
+
|
|
308
|
+
if (parseable === mdFiles.length) {
|
|
309
|
+
pass(`all ${mdFiles.length} agent frontmatter blocks parse as YAML mappings`);
|
|
310
|
+
}
|
|
239
311
|
}
|
|
240
312
|
|
|
241
313
|
// ============================================================================
|