session-orchestrator 3.16.0 → 3.19.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/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +130 -412
- package/README.md +17 -12
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/docs/README.md +2 -1
- package/docs/USER-GUIDE.md +8 -3
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +444 -0
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +306 -6
- package/docs/session-config-template.md +66 -3
- package/docs/telemetry/telemetry-claims.md +204 -0
- package/docs/telemetry.md +158 -0
- package/hooks/_lib/guard-source-loader.mjs +467 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +23 -0
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/skill-invocation-telemetry.mjs +109 -10
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +16 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/compute-grounding-injection.sh +18 -3
- package/scripts/dialectic-deriver.mjs +7 -2
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/auto-dialectic.mjs +11 -2
- package/scripts/lib/auto-dream.mjs +16 -5
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/build-live-signals.mjs +7 -4
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1013 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/context-coverage.mjs +82 -0
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/moc-staleness.mjs +98 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config/worktree-orphans.mjs +138 -0
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +32 -0
- package/scripts/lib/context-coverage-banner.mjs +223 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +136 -30
- package/scripts/lib/dispatcher/rank.mjs +22 -8
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/moc-staleness-banner.mjs +267 -0
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/filters.mjs +88 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/skill-health/join.mjs +35 -9
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/telemetry/anon-id.mjs +141 -0
- package/scripts/lib/telemetry/consent.mjs +299 -0
- package/scripts/lib/telemetry/paths.mjs +27 -0
- package/scripts/lib/telemetry/queue.mjs +287 -0
- package/scripts/lib/telemetry/schema.mjs +384 -0
- package/scripts/lib/telemetry/sync.mjs +312 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vault-status/board-writer.mjs +63 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +32 -6
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/telemetry.mjs +250 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +97 -0
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +108 -14
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +149 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/SKILL.md +1 -1
- package/skills/vault-sync/package-lock.json +3 -3
- package/skills/vault-sync/validator.mjs +228 -40
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -62,6 +62,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
62
62
|
import { z } from 'zod';
|
|
63
63
|
import YAML from 'yaml';
|
|
64
64
|
import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
|
|
65
|
+
import { findSessionConfigBlock } from '../../scripts/lib/config/section-extractor.mjs';
|
|
65
66
|
import {
|
|
66
67
|
computeSchemaHash,
|
|
67
68
|
writeBaseline,
|
|
@@ -159,14 +160,27 @@ const _schemaHash = (() => {
|
|
|
159
160
|
const args = process.argv.slice(2);
|
|
160
161
|
const checkExpires = args.includes('--check-expires');
|
|
161
162
|
|
|
162
|
-
// Parse --mode <hard|warn|off|baseline|diff|full> (default: hard)
|
|
163
|
+
// Parse --mode <hard|strict|warn|off|baseline|diff|full> (default: hard)
|
|
163
164
|
// hard — legacy alias; identical to full enforcement
|
|
165
|
+
// strict — alias of hard (#835). The Session Config schema
|
|
166
|
+
// (scripts/lib/config-schema.mjs VAULT_MODE_VALUES) uses
|
|
167
|
+
// strict|warn|off, so a caller forwarding the configured value
|
|
168
|
+
// verbatim would otherwise hit exit 2. Normalized to 'hard' at
|
|
169
|
+
// parse time so exactly one internal value flows downstream.
|
|
164
170
|
// full — enforce: exit 1 on errors
|
|
165
171
|
// warn — report but exit 0
|
|
166
172
|
// off — skip entirely
|
|
167
173
|
// baseline — write snapshot then exit 0
|
|
168
174
|
// diff — compare against snapshot, emit JSON diff
|
|
169
175
|
// Parse --exclude <glob> (repeatable)
|
|
176
|
+
const MODE_VALUES = new Set(['hard', 'strict', 'warn', 'off', 'baseline', 'diff', 'full']);
|
|
177
|
+
const MODE_EXPECTED = 'hard|strict|warn|off|baseline|diff|full';
|
|
178
|
+
|
|
179
|
+
/** Normalize CLI mode aliases to the single internal value used downstream. */
|
|
180
|
+
function normalizeMode(v) {
|
|
181
|
+
return v === 'strict' ? 'hard' : v;
|
|
182
|
+
}
|
|
183
|
+
|
|
170
184
|
let mode = 'hard';
|
|
171
185
|
const excludePatterns = [];
|
|
172
186
|
|
|
@@ -204,22 +218,22 @@ for (let i = 0; i < args.length; i++) {
|
|
|
204
218
|
const a = args[i];
|
|
205
219
|
if (a === '--mode') {
|
|
206
220
|
const v = args[i + 1];
|
|
207
|
-
if (v
|
|
208
|
-
mode = v;
|
|
221
|
+
if (MODE_VALUES.has(v)) {
|
|
222
|
+
mode = normalizeMode(v);
|
|
209
223
|
} else {
|
|
210
224
|
process.stderr.write(
|
|
211
|
-
`validator.mjs: invalid --mode value "${v}" (expected
|
|
225
|
+
`validator.mjs: invalid --mode value "${v}" (expected ${MODE_EXPECTED})\n`,
|
|
212
226
|
);
|
|
213
227
|
process.exit(2);
|
|
214
228
|
}
|
|
215
229
|
i++;
|
|
216
230
|
} else if (a.startsWith('--mode=')) {
|
|
217
231
|
const v = a.slice('--mode='.length);
|
|
218
|
-
if (v
|
|
219
|
-
mode = v;
|
|
232
|
+
if (MODE_VALUES.has(v)) {
|
|
233
|
+
mode = normalizeMode(v);
|
|
220
234
|
} else {
|
|
221
235
|
process.stderr.write(
|
|
222
|
-
`validator.mjs: invalid --mode value "${v}" (expected
|
|
236
|
+
`validator.mjs: invalid --mode value "${v}" (expected ${MODE_EXPECTED})\n`,
|
|
223
237
|
);
|
|
224
238
|
process.exit(2);
|
|
225
239
|
}
|
|
@@ -283,18 +297,38 @@ function isExcluded(relPath) {
|
|
|
283
297
|
// Returns true if dir contains at least one recognized vault marker:
|
|
284
298
|
// 1. _meta/ directory
|
|
285
299
|
// 2. CLAUDE.md or AGENTS.md (alias — see skills/_shared/instruction-file-resolution.md)
|
|
286
|
-
//
|
|
287
|
-
// file is resolved via resolveInstructionFile() so CLAUDE.md
|
|
288
|
-
// AGENTS.md is accepted on Codex CLI repos.
|
|
300
|
+
// whose `## Session Config` block declares a `vault-sync:` key. The
|
|
301
|
+
// instruction file is resolved via resolveInstructionFile() so CLAUDE.md
|
|
302
|
+
// wins ties and AGENTS.md is accepted on Codex CLI repos.
|
|
289
303
|
// 3. .obsidian/ directory
|
|
304
|
+
//
|
|
305
|
+
// Marker 2 was two whole-file substring tests until #968:
|
|
306
|
+
// content.includes('## Session Config') && content.includes('vault-sync:')
|
|
307
|
+
// A substring test is the wrong instrument for "does this directory look like
|
|
308
|
+
// a vault" in three independent ways, all reachable:
|
|
309
|
+
// (a) `'### Session Config'.includes('## Session Config')` is TRUE (from
|
|
310
|
+
// index 1), so an H3 — or any deeper heading — passed the marker.
|
|
311
|
+
// HTML comment matched. Any document ABOUT session-orchestrator config
|
|
312
|
+
// reads as a vault marker.
|
|
313
|
+
// (b) A `## Session Config` mention in ordinary prose (not at line start)
|
|
314
|
+
// matched, e.g. "see the ## Session Config block" mid-sentence.
|
|
315
|
+
// (c) `vault-sync:` was accepted ANYWHERE in the file — a prose sentence or
|
|
316
|
+
// a doc-comment sufficed; it never had to be a config key, let alone one
|
|
317
|
+
// inside the Session Config block.
|
|
318
|
+
// Misclassifying a directory as a vault is not cosmetic: it makes the
|
|
319
|
+
// validator crawl and enforce vault frontmatter over an arbitrary tree, and
|
|
320
|
+
// (via the cwd branch below) silently adopt it as VAULT_DIR.
|
|
321
|
+
//
|
|
322
|
+
// The fix uses the SSOT block extractor, so the heading must be a real
|
|
323
|
+
// heading LINE and `vault-sync:` must be a key INSIDE that block.
|
|
290
324
|
function isVaultDir(dir) {
|
|
291
325
|
if (existsSync(join(dir, '_meta')) && statSync(join(dir, '_meta')).isDirectory()) return true;
|
|
292
326
|
if (existsSync(join(dir, '.obsidian')) && statSync(join(dir, '.obsidian')).isDirectory()) return true;
|
|
293
327
|
const instr = resolveInstructionFile(dir);
|
|
294
328
|
if (instr) {
|
|
295
329
|
try {
|
|
296
|
-
const
|
|
297
|
-
if (
|
|
330
|
+
const block = findSessionConfigBlock(readFileSync(instr.path, 'utf8'));
|
|
331
|
+
if (block && /^\s*(?:-\s+)?(?:\*\*)?vault-sync(?:\*\*)?\s*:/m.test(block.body)) return true;
|
|
298
332
|
} catch {
|
|
299
333
|
// unreadable instruction file — not a vault marker
|
|
300
334
|
}
|
|
@@ -335,13 +369,29 @@ if (process.env.VAULT_DIR) {
|
|
|
335
369
|
}
|
|
336
370
|
}
|
|
337
371
|
|
|
372
|
+
// Directories the crawler never descends into. These are structurally
|
|
373
|
+
// invisible: their notes are neither checked NOR available as link targets.
|
|
338
374
|
const EXCLUDED_DIRS = new Set([
|
|
339
375
|
'node_modules',
|
|
340
376
|
'.git',
|
|
341
377
|
'.obsidian',
|
|
342
|
-
'90-archive',
|
|
343
378
|
]);
|
|
344
379
|
|
|
380
|
+
// Top-level directories whose notes are skipped by the CHECK set but remain in
|
|
381
|
+
// the link-target register (#833). Before this split, '90-archive' lived in
|
|
382
|
+
// EXCLUDED_DIRS, so archiving a note silently turned every inbound
|
|
383
|
+
// [[wiki-link]] into a dangling-link warning. The register and the check-set
|
|
384
|
+
// are now decoupled: walk() still visits these notes (so they resolve links),
|
|
385
|
+
// the validation loop skips them (so their frontmatter never blocks a close).
|
|
386
|
+
const CHECK_EXCLUDED_TOP_DIRS = new Set(['90-archive']);
|
|
387
|
+
|
|
388
|
+
/** True when relPath's FIRST path segment is a check-excluded top-level dir. */
|
|
389
|
+
function isArchived(relPath) {
|
|
390
|
+
const p = String(relPath).replace(/\\/g, '/');
|
|
391
|
+
const top = p.split('/', 1)[0];
|
|
392
|
+
return CHECK_EXCLUDED_TOP_DIRS.has(top);
|
|
393
|
+
}
|
|
394
|
+
|
|
345
395
|
function emit(obj) {
|
|
346
396
|
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
347
397
|
}
|
|
@@ -354,6 +404,7 @@ if (mode === 'off') {
|
|
|
354
404
|
vault_dir: vaultDir,
|
|
355
405
|
files_checked: 0,
|
|
356
406
|
excluded_count: 0,
|
|
407
|
+
archived_skipped_count: 0,
|
|
357
408
|
files_skipped_no_frontmatter: 0,
|
|
358
409
|
errors: [],
|
|
359
410
|
warnings: [],
|
|
@@ -415,16 +466,86 @@ function parseFrontmatter(raw) {
|
|
|
415
466
|
}
|
|
416
467
|
}
|
|
417
468
|
|
|
418
|
-
// ──
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
469
|
+
// ── Wiki-link regex — captures link body for target parsing ─────────────────
|
|
470
|
+
// NOTE: intentionally NOT module-level. A shared `/g` regex driven by
|
|
471
|
+
// `.exec()` in a loop carries `lastIndex` state across calls; the previous
|
|
472
|
+
// module-level `WIKILINK_RE` was safe only because the loop always ran to
|
|
473
|
+
// exhaustion (never an early return/continue). Constructing a fresh regex
|
|
474
|
+
// per `extractWikiLinks()` call removes that hazard structurally rather than
|
|
475
|
+
// relying on "the loop happens to always finish" (#852).
|
|
476
|
+
function wikilinkRegex() {
|
|
477
|
+
return /\[\[([^\]]+)\]\]/g;
|
|
424
478
|
}
|
|
425
479
|
|
|
426
|
-
// ──
|
|
427
|
-
|
|
480
|
+
// ── Code-span stripping (#852, hardened post-review) ────────────────────────
|
|
481
|
+
// A wikilink written as inline code (`` `[[target]]` ``) or inside a fenced /
|
|
482
|
+
// indented code block is pedagogical prose ABOUT the Obsidian wiki-link
|
|
483
|
+
// convention (#159 pattern: keep named invariants in backticks), not a real
|
|
484
|
+
// link, and must never surface a dangling-wiki-link warning. All span kinds
|
|
485
|
+
// are blanked out (non-newline chars -> space, newlines preserved) BEFORE the
|
|
486
|
+
// wikilink regex runs, so their `[[...]]` content is invisible to
|
|
487
|
+
// extractWikiLinks() while line count / offsets stay stable for any future
|
|
488
|
+
// position-aware consumer.
|
|
489
|
+
//
|
|
490
|
+
// NOTE: intentionally functions, not module-level `/g` regex constants. Every
|
|
491
|
+
// call below builds a fresh RegExp so no shared `lastIndex` state can leak
|
|
492
|
+
// across calls (same rationale as wikilinkRegex() above, #852).
|
|
493
|
+
//
|
|
494
|
+
// Order: fenced blocks (multi-line) -> indented code blocks (line-based) ->
|
|
495
|
+
// inline spans (single/double backtick, same line only). Fenced blocks run
|
|
496
|
+
// first because they are the only multi-line shape; the other two are
|
|
497
|
+
// line-local and their relative order does not change the result (a line
|
|
498
|
+
// already blanked by an earlier pass is all-spaces and matches idempotently
|
|
499
|
+
// under either later pass).
|
|
500
|
+
//
|
|
501
|
+
// Fenced-block fix (post-#852 QA review): the ORIGINAL `/```[\s\S]*?```/g`
|
|
502
|
+
// paired ANY two ``` runs in the whole file — including a ``` merely
|
|
503
|
+
// MENTIONED mid-sentence in prose — which silently blanked (and hid dangling
|
|
504
|
+
// links inside) everything between an unrelated mention and the next real
|
|
505
|
+
// fence. Real CommonMark fences must open at the START of a line (up to 3
|
|
506
|
+
// leading spaces); this regex is now line-anchored so a mid-line mention can
|
|
507
|
+
// never open a fence. The closing fence must reuse the SAME fence character
|
|
508
|
+
// (backtick vs tilde) with a run of at least 3 — `(?:\1){3,}` repeats the
|
|
509
|
+
// single captured fence character, so a backtick fence cannot be closed by a
|
|
510
|
+
// tilde run or vice versa. Also handles `~~~` fences and fenced blocks that
|
|
511
|
+
// carry an info string (e.g. ```` ```md ````) — the info string is just the
|
|
512
|
+
// rest of the opening line, already consumed by `[^\n]*`.
|
|
513
|
+
function fencedCodeRegex() {
|
|
514
|
+
return /^ {0,3}(`|~)\1{2,}[^\n]*\n[\s\S]*?^ {0,3}(?:\1){3,}[ \t]*$/gm;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Indented code blocks (4+ leading spaces, or a leading tab) are CommonMark
|
|
518
|
+
// code too. Required to be flanked by a blank line (or start/end of text) on
|
|
519
|
+
// both sides — mirrors CommonMark's "separated from surrounding paragraph
|
|
520
|
+
// text" rule closely enough to avoid blanking ordinary 4-space-indented list
|
|
521
|
+
// continuation text that isn't actually a code block, while still catching
|
|
522
|
+
// the FP shape this fixes.
|
|
523
|
+
function indentedCodeRegex() {
|
|
524
|
+
return /(?<=^|\n\n)(?: {4,}|\t)[^\n]*(?:\n(?: {4,}|\t)[^\n]*)*/g;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Inline code spans: single-backtick (`` ` `` ... `` ` ``) or double-backtick
|
|
528
|
+
// (`` `` `` ... `` `` ``, used when the wrapped content itself contains a
|
|
529
|
+
// backtick) delimiters, same line only. The closing delimiter must be the
|
|
530
|
+
// SAME LENGTH as the opening one (`\1` backreference on the captured 1-2
|
|
531
|
+
// backtick run) so a double-backtick span isn't mis-closed by the first lone
|
|
532
|
+
// backtick inside its own content — the over-strip hazard called out in
|
|
533
|
+
// #852. A lone/stray backtick with no same-length partner on the same line
|
|
534
|
+
// therefore matches nothing and is left as plain text.
|
|
535
|
+
function inlineCodeRegex() {
|
|
536
|
+
return /(`{1,2})[^\n]*?\1(?!`)/g;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function blank(match) {
|
|
540
|
+
return match.replace(/[^\n]/g, ' ');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function stripCodeSpans(text) {
|
|
544
|
+
let out = text.replace(fencedCodeRegex(), blank);
|
|
545
|
+
out = out.replace(indentedCodeRegex(), blank);
|
|
546
|
+
out = out.replace(inlineCodeRegex(), blank);
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
428
549
|
|
|
429
550
|
function findAliasSeparatorIndex(linkBody) {
|
|
430
551
|
for (let i = 0; i < linkBody.length; i++) {
|
|
@@ -441,18 +562,81 @@ function extractWikiLinkTarget(linkBody) {
|
|
|
441
562
|
}
|
|
442
563
|
|
|
443
564
|
function extractWikiLinks(content) {
|
|
565
|
+
const stripped = stripCodeSpans(content);
|
|
444
566
|
const targets = new Set();
|
|
567
|
+
const re = wikilinkRegex();
|
|
445
568
|
let m;
|
|
446
|
-
while ((m =
|
|
569
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
447
570
|
const target = extractWikiLinkTarget(m[1]);
|
|
448
571
|
if (target.length > 0) targets.add(target);
|
|
449
572
|
}
|
|
450
573
|
return [...targets];
|
|
451
574
|
}
|
|
452
575
|
|
|
576
|
+
// ── Pass 1: read every file ONCE ────────────────────────────────────────────
|
|
577
|
+
// Builds the record set the link-target register (below) and the validation
|
|
578
|
+
// loop (further down) both consume. Net-neutral on I/O versus the previous
|
|
579
|
+
// shape, which read the file once but ran the frontmatter match twice.
|
|
580
|
+
// A malformed note must never abort this pass — every failure is recorded on
|
|
581
|
+
// the record and surfaced later by the validation loop.
|
|
582
|
+
const records = [];
|
|
583
|
+
for (const file of mdFiles) {
|
|
584
|
+
const rel = relative(vaultDir, file);
|
|
585
|
+
let raw;
|
|
586
|
+
try {
|
|
587
|
+
raw = readFileSync(file, 'utf8');
|
|
588
|
+
} catch (err) {
|
|
589
|
+
records.push({ file, rel, readError: err.message || String(err) });
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
let fm;
|
|
593
|
+
let links;
|
|
594
|
+
try {
|
|
595
|
+
fm = parseFrontmatter(raw);
|
|
596
|
+
const body = raw.slice(raw.match(FRONTMATTER_RE)?.[0].length || 0);
|
|
597
|
+
links = extractWikiLinks(body);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
fm = { hasFrontmatter: true, parseError: err.message || String(err) };
|
|
600
|
+
links = [];
|
|
601
|
+
}
|
|
602
|
+
records.push({ file, rel, fm, links });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── Link-target register ────────────────────────────────────────────────────
|
|
606
|
+
// Keys: basename-without-ext, frontmatter `id`, and every frontmatter `aliases`
|
|
607
|
+
// entry (#833). Keys are normalized NFC + lowercase at BOTH insert and lookup:
|
|
608
|
+
// NFC is required, not cosmetic — APFS returns decomposed (NFD) filenames and
|
|
609
|
+
// this is a German-language corpus, so "Übung" from a filename and "Übung" from
|
|
610
|
+
// a YAML string would otherwise be different strings. Values are already
|
|
611
|
+
// arrays, so a case-collision (Topic.md + topic.md) merely appends.
|
|
612
|
+
const fileIndex = new Map(); // normalized key -> [absolute paths]
|
|
613
|
+
|
|
614
|
+
function indexKey(k) {
|
|
615
|
+
return String(k).normalize('NFC').toLowerCase();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function addIndexKey(key, file) {
|
|
619
|
+
if (typeof key !== 'string' || key.length === 0) return;
|
|
620
|
+
const k = indexKey(key);
|
|
621
|
+
if (!fileIndex.has(k)) fileIndex.set(k, []);
|
|
622
|
+
fileIndex.get(k).push(file);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (const rec of records) {
|
|
626
|
+
addIndexKey(basename(rec.file, '.md'), rec.file);
|
|
627
|
+
const data = rec.fm && rec.fm.hasFrontmatter && !rec.fm.parseError ? rec.fm.data : null;
|
|
628
|
+
if (!data || typeof data !== 'object') continue;
|
|
629
|
+
// Type-guard: `id` must be a string, `aliases` an array of strings. A note
|
|
630
|
+
// with `aliases: some-scalar` contributes no alias keys instead of throwing.
|
|
631
|
+
if (typeof data.id === 'string') addIndexKey(data.id, rec.file);
|
|
632
|
+
if (Array.isArray(data.aliases)) {
|
|
633
|
+
for (const alias of data.aliases) addIndexKey(alias, rec.file);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
453
637
|
function resolveWikiLink(target, sourceFile) {
|
|
454
638
|
// Target may be a bare name ("my-note") or a path ("01-projects/foo/_overview").
|
|
455
|
-
// Try exact path first (relative to vault), then
|
|
639
|
+
// Try exact path first (relative to vault), then register lookup.
|
|
456
640
|
const candidate1 = resolve(vaultDir, target.endsWith('.md') ? target : target + '.md');
|
|
457
641
|
if (existsSync(candidate1)) return true;
|
|
458
642
|
|
|
@@ -463,9 +647,8 @@ function resolveWikiLink(target, sourceFile) {
|
|
|
463
647
|
);
|
|
464
648
|
if (existsSync(candidate2)) return true;
|
|
465
649
|
|
|
466
|
-
// Try
|
|
467
|
-
|
|
468
|
-
if (fileIndex.has(key)) return true;
|
|
650
|
+
// Try register lookup anywhere in the vault (basename / id / alias)
|
|
651
|
+
if (fileIndex.has(indexKey(basename(target, '.md')))) return true;
|
|
469
652
|
|
|
470
653
|
return false;
|
|
471
654
|
}
|
|
@@ -476,29 +659,31 @@ const warnings = [];
|
|
|
476
659
|
let filesChecked = 0;
|
|
477
660
|
let filesSkippedNoFrontmatter = 0;
|
|
478
661
|
let excludedCount = 0;
|
|
662
|
+
let archivedSkippedCount = 0;
|
|
479
663
|
|
|
480
664
|
const todayIso = new Date().toISOString().slice(0, 10);
|
|
481
665
|
|
|
482
|
-
|
|
483
|
-
|
|
666
|
+
// ── Pass 2: validate ────────────────────────────────────────────────────────
|
|
667
|
+
for (const rec of records) {
|
|
668
|
+
const { file, rel, fm } = rec;
|
|
484
669
|
if (isExcluded(rel)) {
|
|
485
670
|
excludedCount++;
|
|
486
671
|
continue;
|
|
487
672
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
673
|
+
// Archived notes stay in the link-target register but are never CHECKED.
|
|
674
|
+
if (isArchived(rel)) {
|
|
675
|
+
archivedSkippedCount++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (rec.readError) {
|
|
492
679
|
errors.push({
|
|
493
680
|
file: rel,
|
|
494
681
|
path: '',
|
|
495
|
-
message: `Cannot read file: ${
|
|
682
|
+
message: `Cannot read file: ${rec.readError}`,
|
|
496
683
|
});
|
|
497
684
|
continue;
|
|
498
685
|
}
|
|
499
686
|
|
|
500
|
-
const fm = parseFrontmatter(raw);
|
|
501
|
-
|
|
502
687
|
if (!fm.hasFrontmatter) {
|
|
503
688
|
filesSkippedNoFrontmatter++;
|
|
504
689
|
continue;
|
|
@@ -527,10 +712,8 @@ for (const file of mdFiles) {
|
|
|
527
712
|
// Even if frontmatter is invalid, still check wiki-links to surface all problems.
|
|
528
713
|
}
|
|
529
714
|
|
|
530
|
-
// Wiki-link check
|
|
531
|
-
const
|
|
532
|
-
const links = extractWikiLinks(body);
|
|
533
|
-
for (const target of links) {
|
|
715
|
+
// Wiki-link check (links were extracted in pass 1)
|
|
716
|
+
for (const target of rec.links) {
|
|
534
717
|
if (!resolveWikiLink(target, file)) {
|
|
535
718
|
warnings.push({
|
|
536
719
|
file: rel,
|
|
@@ -589,6 +772,7 @@ if (mode === 'diff') {
|
|
|
589
772
|
vault_dir: vaultDir,
|
|
590
773
|
files_checked: filesChecked,
|
|
591
774
|
excluded_count: excludedCount,
|
|
775
|
+
archived_skipped_count: archivedSkippedCount,
|
|
592
776
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
593
777
|
errors,
|
|
594
778
|
warnings,
|
|
@@ -608,6 +792,7 @@ if (mode === 'diff') {
|
|
|
608
792
|
vault_dir: vaultDir,
|
|
609
793
|
files_checked: filesChecked,
|
|
610
794
|
excluded_count: excludedCount,
|
|
795
|
+
archived_skipped_count: archivedSkippedCount,
|
|
611
796
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
612
797
|
errors,
|
|
613
798
|
warnings,
|
|
@@ -629,6 +814,7 @@ if (mode === 'diff') {
|
|
|
629
814
|
vault_dir: vaultDir,
|
|
630
815
|
files_checked: filesChecked,
|
|
631
816
|
excluded_count: excludedCount,
|
|
817
|
+
archived_skipped_count: archivedSkippedCount,
|
|
632
818
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
633
819
|
});
|
|
634
820
|
|
|
@@ -640,7 +826,8 @@ if (mode === 'diff') {
|
|
|
640
826
|
}
|
|
641
827
|
|
|
642
828
|
// ── mode=full | hard | warn ───────────────────────────────────────────────
|
|
643
|
-
// 'full' and 'hard' are identical (hard is the legacy alias
|
|
829
|
+
// 'full' and 'hard' are identical (hard is the legacy alias; 'strict' was
|
|
830
|
+
// normalized to 'hard' at parse time — see normalizeMode, #835).
|
|
644
831
|
// In warn mode, errors are reported but the status is "ok" for exit-code purposes.
|
|
645
832
|
// The errors array is still populated so the caller can surface them as warnings.
|
|
646
833
|
const status = hasErrors ? (mode === 'warn' ? 'ok' : 'invalid') : 'ok';
|
|
@@ -650,6 +837,7 @@ emit({
|
|
|
650
837
|
vault_dir: vaultDir,
|
|
651
838
|
files_checked: filesChecked,
|
|
652
839
|
excluded_count: excludedCount,
|
|
840
|
+
archived_skipped_count: archivedSkippedCount,
|
|
653
841
|
files_skipped_no_frontmatter: filesSkippedNoFrontmatter,
|
|
654
842
|
errors,
|
|
655
843
|
warnings,
|
|
@@ -246,7 +246,7 @@ Each agent prompt MUST include:
|
|
|
246
246
|
2. **Full context**: file paths, current code structure, issue description. If a bite-sized executable plan exists at `docs/plans/<feature>.md` for the wave's tasks (see `skills/write-executable-plan/SKILL.md`), include the path in each agent's prompt and instruct the agent to follow the plan's 5-step structure verbatim.
|
|
247
247
|
3. **Acceptance criteria**: measurable definition of done
|
|
248
248
|
4. **Rule references**: the wave's applicable rules are injected automatically as the `<APPLICABLE-RULES>` block produced by `scripts/print-applicable-rules.mjs` (see `wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection (#336/#694)"). The block is computed once per wave from the wave's `allowedPaths` and prepended to every agent prompt — do not hand-copy rule paths into the prompt.
|
|
249
|
-
5. **Testing expectation
|
|
249
|
+
5. **Testing expectation** (need-gated): "Before writing any test, name the concrete bug a NEW test would catch that the existing suite does not. No nameable bug → write NO test and report `no-tests-needed: <reason>` — that is a SUCCESS outcome, not a gap. With a nameable bug: exactly one test for it. Running existing tests is always mandatory."
|
|
250
250
|
6. **Commit instruction**: "Do NOT commit. The coordinator handles commits."
|
|
251
251
|
7. **Turn limit**: Include the maxTurns instruction from `circuit-breaker.md`
|
|
252
252
|
8. **Verification before completion**: Before claiming any task done, run the verification command and quote the evidence inline. See `.claude/rules/verification-before-completion.md`.
|
|
@@ -272,10 +272,13 @@ During this wave, you may propose a learning to the session's memory via the CLI
|
|
|
272
272
|
--subject "one-line title (max 100 chars, no newlines)" \
|
|
273
273
|
--insight "your discovery paragraph (max 2000 chars)" \
|
|
274
274
|
--evidence "concrete proof: code citation / log excerpt / commit ref (max 5000 chars)" \
|
|
275
|
-
--confidence <0.5 to 1.0>
|
|
275
|
+
--confidence <0.5 to 1.0> \
|
|
276
|
+
--file-paths "scripts/lib/a.mjs,scripts/lib/b.mjs"
|
|
276
277
|
|
|
277
278
|
MUST prefix with `SO_WAVE_AGENT=1` — without it the CLI returns exit 3 `rejected-wrong-context`. The env-var is the per-process guard that distinguishes wave-executor agents from coordinator-context invocations.
|
|
278
279
|
|
|
280
|
+
`--file-paths` is optional but strongly encouraged: repo-relative path(s) this learning applies to (repeatable and/or comma-separated, deduped; rejects absolute paths, `..` segments, embedded newlines, entries over 256 chars, and more than 20 entries). Without `--file-paths` this learning can never become `/reconcile`-eligible — the reconciliation engine can only convert a learning into a conditional `.claude/rules/*.md` rule when it carries a non-empty scope (issue #900).
|
|
281
|
+
|
|
279
282
|
Exit code 0 = queued (the coordinator will present at session-end via AskUserQuestion); 1 = quota-exceeded; 2 = rejected-low-confidence (below floor 0.5); 3 = rejected-wrong-context (STATE.md not active OR SO_WAVE_AGENT != "1"); 4 = error (arg validation or internal).
|
|
280
283
|
|
|
281
284
|
Use ONLY when you find a recurring pattern, anti-pattern, or constraint worth carrying into future sessions. The coordinator confirms each proposal before it lands in learnings.jsonl. Do NOT over-propose — quota is bounded per wave.
|
|
@@ -31,6 +31,7 @@ The coordinator determines each agent's status after the wave completes:
|
|
|
31
31
|
1. **Read the agent's final output** and look for the `STATUS:` line
|
|
32
32
|
2. **Map to status**:
|
|
33
33
|
- `STATUS: done` → agent completed successfully
|
|
34
|
+
- `STATUS: no-tests-needed` → **SUCCESS variant, treat exactly like `done`.** test-writer-specific: the agent found no nameable bug the existing suite misses, so writing a test was the wrong move (`.claude/rules/test-value.md`; enum in `agents/schemas/test-writer.schema.json`). NEVER map this to `failed` or carry it over.
|
|
34
35
|
- `STATUS: partial` → agent hit turn limit or couldn't finish (PARTIAL)
|
|
35
36
|
- `STATUS: failed` → agent encountered an error it couldn't recover from (FAILED)
|
|
36
37
|
- No STATUS line found → infer from output: if agent produced changes, mark as `done`; if it reported errors, mark as `failed`; if output is truncated, mark as `partial`
|
|
@@ -45,6 +46,7 @@ The coordinator determines each agent's status after the wave completes:
|
|
|
45
46
|
| Status | Meaning | Trigger | Recovery |
|
|
46
47
|
|--------|---------|---------|----------|
|
|
47
48
|
| **done** | Agent completed all assigned work | Agent reports `STATUS: done` | None needed |
|
|
49
|
+
| **no-tests-needed** | SUCCESS variant of **done** — test-writer found no nameable gap, so no test was the correct outcome | Agent reports `STATUS: no-tests-needed` | None needed — never a failure, never a carryover |
|
|
48
50
|
| **partial** | Agent made progress but couldn't finish | Turn limit hit, or agent reports `STATUS: partial` | Carry forward remaining work to next wave with context |
|
|
49
51
|
| **failed** | Agent couldn't make meaningful progress | Tool errors, invalid assumptions, agent reports `STATUS: failed` | Re-dispatch with corrected instructions and narrower scope |
|
|
50
52
|
| **spiral** | Agent got stuck in an edit loop | Same file edited 3+ times (detected post-wave) | Revert agent's changes, narrow scope, split task if needed |
|