session-orchestrator 3.22.0 → 3.23.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/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +11 -3
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +30 -16
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
* 2. Enumerate tracked .md files under --prd-dir (git ls-files), excluding
|
|
17
17
|
* *.original-uncommitted.md. A missing/empty --prd-dir yields [] → clean
|
|
18
18
|
* report, exit 0 (no crash) — see listTrackedPrds.
|
|
19
|
-
* 3. For each doc: parse
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 3. For each doc: parse an Epic/Issue DECLARATION out of the header region
|
|
20
|
+
* (first ~20 lines, up to the first `## ` section) — frontmatter key, label
|
|
21
|
+
* line, or H1 title suffix. A bare `#NNN` in prose is a CITATION and never
|
|
22
|
+
* counts (`citation-only`); no `#NNN` at all → `no-epic-ref`. Both skip.
|
|
23
|
+
* 4. Session ownership: a doc with uncommitted changes (`uncommitted`) or one
|
|
24
|
+
* committed at/after this session's `started_at` (`foreign-session`) is
|
|
25
|
+
* skipped — it belongs to a parallel session, not to us (#1123).
|
|
26
|
+
* 5. `glab issue view <iid> --output json` → state. Only `closed` refs archive;
|
|
22
27
|
* `opened` and unknown/error states skip (never guess).
|
|
23
|
-
*
|
|
28
|
+
* 6. Closed → archiveFileToVault(...) into <vault>/<--vault-subdir>/. Under
|
|
24
29
|
* --apply the source doc is removed with `git rm`; --dry-run (default) writes
|
|
25
30
|
* NOTHING (not even to the vault).
|
|
26
31
|
*
|
|
@@ -29,8 +34,9 @@
|
|
|
29
34
|
*
|
|
30
35
|
* Exit codes: 0 success · 1 input/config error · 2 system error.
|
|
31
36
|
*
|
|
32
|
-
* Exports (for tests): main, parseEpicRef, readHeaderRegion,
|
|
33
|
-
* epicState,
|
|
37
|
+
* Exports (for tests): main, parseEpicDeclaration, parseEpicRef, readHeaderRegion,
|
|
38
|
+
* listTrackedPrds, epicState, classifyOwnership, readSessionStartedAt,
|
|
39
|
+
* defaultGlabRepo.
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
import { readFileSync } from 'node:fs';
|
|
@@ -43,6 +49,9 @@ import { parseSessionConfig } from './lib/config.mjs';
|
|
|
43
49
|
import { glabRun as defaultGlabRun } from './lib/vault-backfill/glab.mjs';
|
|
44
50
|
import { archiveFileToVault, titleFromMarkdown } from './lib/vault-archive.mjs';
|
|
45
51
|
import { defaultGlabRepo } from './lib/vcs-repo-spec.mjs';
|
|
52
|
+
import { readLock } from './lib/session-lock.mjs';
|
|
53
|
+
import { resolveStateMdPath } from './lib/state-md/frontmatter-mutators.mjs';
|
|
54
|
+
import { parseStateMd } from './lib/state-md/yaml-parser.mjs';
|
|
46
55
|
|
|
47
56
|
const DEFAULT_PRD_DIR = 'docs/prd';
|
|
48
57
|
const DEFAULT_VAULT_SUBDIR = '01-projects/session-orchestrator/prd';
|
|
@@ -74,14 +83,184 @@ export { defaultGlabRepo };
|
|
|
74
83
|
// Pure helpers
|
|
75
84
|
// ---------------------------------------------------------------------------
|
|
76
85
|
|
|
86
|
+
// A DECLARATION line names the doc's own tracking Epic/Issue. Three accepted
|
|
87
|
+
// spellings, all anchored at line start so prose can never reach them:
|
|
88
|
+
// (a) `**Epic:** #1113` — colon INSIDE the bold (the shape 2 of 4 live PRDs use)
|
|
89
|
+
// (b) `**Epic**: #1113` — colon outside the bold
|
|
90
|
+
// (c) `Source: docs/prd/x.md (#786)` — unbolded (the plan-header shape,
|
|
91
|
+
// skills/write-executable-plan/SKILL.md:193)
|
|
92
|
+
// The optional leading `-`/`*` covers list items (`- **Issue:** #366`).
|
|
93
|
+
const LABEL_WORD = String.raw`(?:parent[ \t]+)?(?:epic|issues?|source)`;
|
|
94
|
+
const LABEL_LINE_RE = new RegExp(
|
|
95
|
+
String.raw`^[-*]?[ \t]*(?:` +
|
|
96
|
+
String.raw`\*\*[ \t]*${LABEL_WORD}[ \t]*:[ \t]*\*\*` +
|
|
97
|
+
String.raw`|\*\*[ \t]*${LABEL_WORD}[ \t]*\*\*[ \t]*:` +
|
|
98
|
+
String.raw`|${LABEL_WORD}[ \t]*:` +
|
|
99
|
+
String.raw`)[ \t]*(\S.*)$`,
|
|
100
|
+
'i',
|
|
101
|
+
);
|
|
102
|
+
const FRONTMATTER_LINE_RE = /^(?:epic|issues?)[ \t]*:[ \t]*\[?[ \t]*['"]?#?(\d+)/i;
|
|
103
|
+
const TITLE_SUFFIX_RE = /^#[ \t]+.*\(#(\d+)\)[ \t]*$/;
|
|
104
|
+
const ANY_IID_RE = /#(\d+)/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse the doc's DECLARED Epic/Issue out of its header region.
|
|
108
|
+
*
|
|
109
|
+
* Precedence (first hit wins): frontmatter key → label line → H1 title suffix.
|
|
110
|
+
* A bare `#NNN` in prose is a CITATION and NEVER counts — that conflation is
|
|
111
|
+
* what deleted a live PRD on 2026-08-22 (#1112): a quoted, long-closed `#214`
|
|
112
|
+
* inside a blockquote read as the doc's own Epic, so the doc looked finished.
|
|
113
|
+
* The rule therefore prefers, by construction, documents that carry LESS
|
|
114
|
+
* context — hence the anchored line shapes below.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} headerText — output of readHeaderRegion().
|
|
117
|
+
* @returns {{ iid: string, via: 'frontmatter'|'label'|'title' }|null}
|
|
118
|
+
*/
|
|
119
|
+
export function parseEpicDeclaration(headerText) {
|
|
120
|
+
const lines = String(headerText).split(/\r?\n/);
|
|
121
|
+
|
|
122
|
+
// (1) Frontmatter — only inside the LEADING `---` block, never a stray
|
|
123
|
+
// `epic:` line further down the header.
|
|
124
|
+
//
|
|
125
|
+
// A leading `---` alone does NOT make a frontmatter block: in Markdown it is
|
|
126
|
+
// also a thematic break, and a doc that opens with one would otherwise have
|
|
127
|
+
// its BODY read as frontmatter (a body line `epic: 1113` — or any `issue:`
|
|
128
|
+
// note — would then win over the real declaration below). Require a CLOSING
|
|
129
|
+
// `---` inside the header region; without it, fall through to label/title.
|
|
130
|
+
if (lines[0]?.trim() === '---') {
|
|
131
|
+
const close = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
|
|
132
|
+
if (close !== -1) {
|
|
133
|
+
for (let i = 1; i < close; i++) {
|
|
134
|
+
const m = FRONTMATTER_LINE_RE.exec(lines[i]);
|
|
135
|
+
if (m) return { iid: m[1], via: 'frontmatter' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// (2) Label line — the first `#NNN` on a recognised label line. Taking the
|
|
141
|
+
// first is correct for `**Epic:** #1048 · **Sub-Issues:** #1049 …`: the
|
|
142
|
+
// primary declaration leads, the sub-issues trail.
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const label = LABEL_LINE_RE.exec(line);
|
|
145
|
+
if (!label) continue;
|
|
146
|
+
const iid = ANY_IID_RE.exec(label[1]);
|
|
147
|
+
if (iid) return { iid: iid[1], via: 'label' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// (3) H1 title suffix — `# Feature: Foo (#1113)`.
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const m = TITLE_SUFFIX_RE.exec(line);
|
|
153
|
+
if (m) return { iid: m[1], via: 'title' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
/**
|
|
78
|
-
*
|
|
160
|
+
* @deprecated Use {@link parseEpicDeclaration} — it also reports HOW the iid was
|
|
161
|
+
* declared, which the caller needs to tell `citation-only` from `no-epic-ref`.
|
|
162
|
+
* Retained as a thin shim for existing importers.
|
|
79
163
|
* @param {string} headerText
|
|
80
|
-
* @returns {string|null} the
|
|
164
|
+
* @returns {string|null} the declared iid as a string, or null when absent.
|
|
81
165
|
*/
|
|
82
166
|
export function parseEpicRef(headerText) {
|
|
83
|
-
|
|
84
|
-
|
|
167
|
+
return parseEpicDeclaration(headerText)?.iid ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Decide whether a doc belongs to THIS session or to a parallel one (#1123).
|
|
172
|
+
*
|
|
173
|
+
* Author IDENTITY carries no signal here — every session commits as the same
|
|
174
|
+
* human — so the discriminator is commit TIME against this session's
|
|
175
|
+
* `started_at`, taken as MAX(author date, committer date) so a rebased or
|
|
176
|
+
* cherry-picked peer commit cannot read as old.
|
|
177
|
+
* Fail-closed in every ambiguous direction: a failed git probe,
|
|
178
|
+
* an unparseable commit timestamp, or a missing `sessionStartedAt` all yield
|
|
179
|
+
* `'foreign'`, because 'foreign' is the verdict that does NOT delete.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} args
|
|
182
|
+
* @param {string} args.repoRoot
|
|
183
|
+
* @param {string} args.rel — repo-relative doc path.
|
|
184
|
+
* @param {string|null|undefined} args.sessionStartedAt — ISO-8601.
|
|
185
|
+
* @param {(args: string[]) => { ok: boolean, stdout: string, stderr: string }} args.gitRunFn
|
|
186
|
+
* @returns {'mine'|'foreign'|'uncommitted'} never throws.
|
|
187
|
+
*/
|
|
188
|
+
export function classifyOwnership({ repoRoot, rel, sessionStartedAt, gitRunFn }) {
|
|
189
|
+
let status;
|
|
190
|
+
try {
|
|
191
|
+
status = gitRunFn(['-C', repoRoot, 'status', '--porcelain', '--', rel]);
|
|
192
|
+
} catch {
|
|
193
|
+
return 'foreign';
|
|
194
|
+
}
|
|
195
|
+
if (!status?.ok) return 'foreign';
|
|
196
|
+
// Non-empty porcelain output = staged, unstaged or untracked (`??`) — either
|
|
197
|
+
// way there is work in the tree that a `git rm` would destroy.
|
|
198
|
+
if (String(status.stdout).trim() !== '') return 'uncommitted';
|
|
199
|
+
|
|
200
|
+
const startedMs = Date.parse(String(sessionStartedAt ?? ''));
|
|
201
|
+
if (Number.isNaN(startedMs)) return 'foreign';
|
|
202
|
+
|
|
203
|
+
let log;
|
|
204
|
+
try {
|
|
205
|
+
// BOTH timestamps, newest wins. `%aI` alone is the AUTHOR date, which
|
|
206
|
+
// survives rebase, cherry-pick and `--amend` UNCHANGED — a peer session's
|
|
207
|
+
// doc rebased onto this branch keeps its old author date and would read as
|
|
208
|
+
// 'mine', i.e. as ours to `git rm`. `%cI` (committer date) moves with every
|
|
209
|
+
// rewrite, so MAX(author, committer) is the moment the object entered THIS
|
|
210
|
+
// history — the quantity the session-ownership guard actually needs.
|
|
211
|
+
log = gitRunFn(['-C', repoRoot, 'log', '-1', '--format=%aI%n%cI', '--', rel]);
|
|
212
|
+
} catch {
|
|
213
|
+
return 'foreign';
|
|
214
|
+
}
|
|
215
|
+
if (!log?.ok) return 'foreign';
|
|
216
|
+
const stamps = String(log.stdout)
|
|
217
|
+
.split(/\r?\n/)
|
|
218
|
+
.map((s) => Date.parse(s.trim()))
|
|
219
|
+
.filter((n) => !Number.isNaN(n));
|
|
220
|
+
if (stamps.length === 0) return 'foreign';
|
|
221
|
+
const committedMs = Math.max(...stamps);
|
|
222
|
+
|
|
223
|
+
return committedMs >= startedMs ? 'foreign' : 'mine';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve THIS session's `started_at` (ISO-8601): the session lock first, the
|
|
228
|
+
* STATE.md frontmatter as fallback.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} repoRoot
|
|
231
|
+
* @returns {string|null} null when neither source yields a parseable timestamp.
|
|
232
|
+
*/
|
|
233
|
+
export function readSessionStartedAt(repoRoot) {
|
|
234
|
+
try {
|
|
235
|
+
const startedAt = readLock({ repoRoot })?.started_at;
|
|
236
|
+
if (typeof startedAt === 'string' && !Number.isNaN(Date.parse(startedAt))) return startedAt;
|
|
237
|
+
} catch {
|
|
238
|
+
// fall through to STATE.md
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const parsed = parseStateMd(readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
|
|
242
|
+
const startedAt = parsed?.frontmatter?.started_at;
|
|
243
|
+
if (typeof startedAt === 'string' && !Number.isNaN(Date.parse(startedAt))) return startedAt;
|
|
244
|
+
} catch {
|
|
245
|
+
// no STATE.md, or unreadable → null (fail-closed at the call site)
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Read the issue iids this session declared in STATE.md frontmatter (`issues:`).
|
|
252
|
+
* @param {string} repoRoot
|
|
253
|
+
* @returns {Set<string>} empty when STATE.md is absent or carries no list.
|
|
254
|
+
*/
|
|
255
|
+
function readSessionIssues(repoRoot) {
|
|
256
|
+
try {
|
|
257
|
+
const parsed = parseStateMd(readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
|
|
258
|
+
const issues = parsed?.frontmatter?.issues;
|
|
259
|
+
if (Array.isArray(issues)) return new Set(issues.map((n) => String(n)));
|
|
260
|
+
} catch {
|
|
261
|
+
// absent/unreadable → empty set
|
|
262
|
+
}
|
|
263
|
+
return new Set();
|
|
85
264
|
}
|
|
86
265
|
|
|
87
266
|
/**
|
|
@@ -167,7 +346,8 @@ function printHelp() {
|
|
|
167
346
|
|
|
168
347
|
USAGE
|
|
169
348
|
node scripts/archive-closed-prds.mjs [--dry-run|--apply] [--json]
|
|
170
|
-
[--prd-dir DIR] [--vault-subdir DIR] [--
|
|
349
|
+
[--prd-dir DIR] [--vault-subdir DIR] [--glab-repo SPEC]
|
|
350
|
+
[--ignore-session-guard] [--owned-issues-only] [--help]
|
|
171
351
|
|
|
172
352
|
FLAGS
|
|
173
353
|
--dry-run (default) Plan the archive; write NOTHING (not even the vault).
|
|
@@ -180,6 +360,16 @@ FLAGS
|
|
|
180
360
|
shell wrapper and uses the ambient GITLAB_HOST). When
|
|
181
361
|
omitted, the spec is auto-detected from the local git
|
|
182
362
|
remote (prefers 'gitlab', else 'origin').
|
|
363
|
+
--ignore-session-guard
|
|
364
|
+
Escape hatch for a CATCH-UP SWEEP: also consider docs last
|
|
365
|
+
committed at/after this session's start (normally skipped
|
|
366
|
+
as 'foreign-session', because they belong to a parallel
|
|
367
|
+
session — #1123). Docs with uncommitted changes are STILL
|
|
368
|
+
skipped; this flag never widens that guard.
|
|
369
|
+
--owned-issues-only
|
|
370
|
+
Opt-in: archive only docs whose declared iid appears in
|
|
371
|
+
this session's STATE.md 'issues:' list. Everything else
|
|
372
|
+
skips as 'epic-#NNN-not-owned'.
|
|
183
373
|
-h, --help Show this help and exit.
|
|
184
374
|
|
|
185
375
|
EXIT CODES
|
|
@@ -229,6 +419,8 @@ function printHuman(archived, skipped, isDryRun, vaultDir, vaultSubdir) {
|
|
|
229
419
|
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
|
|
230
420
|
* — forwarded to parseSessionConfig. Tests pass `{ env: {}, ownerConfig: undefined }`
|
|
231
421
|
* for hermetic, owner.yaml-free vault-dir resolution (issue #653 bleed guard).
|
|
422
|
+
* @param {string|null} [opts.sessionStartedAt] — ISO-8601 override for the
|
|
423
|
+
* session-ownership guard (#1123). Omitted → readSessionStartedAt(root).
|
|
232
424
|
* @returns {{ code: 0|1|2, archived: object[], skipped: object[], vaultDir?: string, dryRun?: boolean }}
|
|
233
425
|
*/
|
|
234
426
|
export function main({
|
|
@@ -238,6 +430,7 @@ export function main({
|
|
|
238
430
|
gitRunFn = defaultGitRun,
|
|
239
431
|
now,
|
|
240
432
|
hostPaths,
|
|
433
|
+
sessionStartedAt,
|
|
241
434
|
} = {}) {
|
|
242
435
|
// ── Parse flags ──────────────────────────────────────────────────────────
|
|
243
436
|
let apply = false;
|
|
@@ -247,6 +440,8 @@ export function main({
|
|
|
247
440
|
let prdDir = DEFAULT_PRD_DIR;
|
|
248
441
|
let vaultSubdir = DEFAULT_VAULT_SUBDIR;
|
|
249
442
|
let glabRepo;
|
|
443
|
+
let ignoreSessionGuard = false;
|
|
444
|
+
let ownedIssuesOnly = false;
|
|
250
445
|
|
|
251
446
|
for (let i = 0; i < argv.length; i++) {
|
|
252
447
|
const a = argv[i];
|
|
@@ -260,6 +455,8 @@ export function main({
|
|
|
260
455
|
else if (a.startsWith('--vault-subdir=')) vaultSubdir = a.slice('--vault-subdir='.length);
|
|
261
456
|
else if (a === '--glab-repo') glabRepo = argv[++i];
|
|
262
457
|
else if (a.startsWith('--glab-repo=')) glabRepo = a.slice('--glab-repo='.length);
|
|
458
|
+
else if (a === '--ignore-session-guard') ignoreSessionGuard = true;
|
|
459
|
+
else if (a === '--owned-issues-only') ownedIssuesOnly = true;
|
|
263
460
|
else {
|
|
264
461
|
process.stderr.write(`archive-closed-prds: unknown argument: ${a}\n`);
|
|
265
462
|
process.stderr.write('Run with --help for usage.\n');
|
|
@@ -314,6 +511,16 @@ export function main({
|
|
|
314
511
|
// the local git remote (host-agnostic; nothing host-specific committed).
|
|
315
512
|
const effectiveGlabRepo = glabRepo ?? defaultGlabRepo(root, gitRunFn);
|
|
316
513
|
|
|
514
|
+
// ── Session identity (ownership guard, #1123) ────────────────────────────
|
|
515
|
+
const startedAt = sessionStartedAt !== undefined ? sessionStartedAt : readSessionStartedAt(root);
|
|
516
|
+
if (!startedAt && !ignoreSessionGuard) {
|
|
517
|
+
warn(
|
|
518
|
+
'archive-closed-prds: no session started_at (session.lock/STATE.md) — every doc is treated ' +
|
|
519
|
+
'as foreign-session. Use --ignore-session-guard for a deliberate catch-up sweep.',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const ownedIssues = ownedIssuesOnly ? readSessionIssues(root) : null;
|
|
523
|
+
|
|
317
524
|
// ── Enumerate + classify PRDs ────────────────────────────────────────────
|
|
318
525
|
const prds = listTrackedPrds(root, prdDir, gitRunFn);
|
|
319
526
|
const archived = [];
|
|
@@ -331,20 +538,51 @@ export function main({
|
|
|
331
538
|
continue;
|
|
332
539
|
}
|
|
333
540
|
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
|
|
541
|
+
const decl = parseEpicDeclaration(header);
|
|
542
|
+
if (!decl) {
|
|
543
|
+
// Distinguish "nothing to go on" from "only a CITATION to go on" — the
|
|
544
|
+
// latter is the #1112 shape and the more dangerous of the two, because a
|
|
545
|
+
// quoted closed issue reads exactly like a declared one.
|
|
546
|
+
const citationOnly = ANY_IID_RE.test(header);
|
|
547
|
+
skipped.push({ source: rel, reason: citationOnly ? 'citation-only' : 'no-epic-ref' });
|
|
548
|
+
warn(
|
|
549
|
+
citationOnly
|
|
550
|
+
? `archive-closed-prds: ${rel} header has #NNN only as a citation, not a declaration — skipped (#1112).`
|
|
551
|
+
: `archive-closed-prds: no Epic reference in header of ${rel} — skipped (never guess).`,
|
|
552
|
+
);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const { iid, via } = decl;
|
|
556
|
+
|
|
557
|
+
// Session ownership BEFORE any glab call: a doc a parallel session just
|
|
558
|
+
// committed is not ours to archive, whatever its Epic's state (#1123).
|
|
559
|
+
const ownership = classifyOwnership({ repoRoot: root, rel, sessionStartedAt: startedAt, gitRunFn });
|
|
560
|
+
if (ownership === 'uncommitted') {
|
|
561
|
+
skipped.push({ source: rel, reason: 'uncommitted', iid, via, ownership });
|
|
562
|
+
warn(`archive-closed-prds: ${rel} has uncommitted changes — skipped (never 'git rm' live work).`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (ownership === 'foreign' && !ignoreSessionGuard) {
|
|
566
|
+
skipped.push({ source: rel, reason: 'foreign-session', iid, via, ownership });
|
|
567
|
+
warn(
|
|
568
|
+
`archive-closed-prds: ${rel} was last committed at/after this session's start — skipped ` +
|
|
569
|
+
'(belongs to a parallel session; --ignore-session-guard overrides).',
|
|
570
|
+
);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (ownedIssues && !ownedIssues.has(String(iid))) {
|
|
575
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-not-owned`, iid, via, ownership });
|
|
338
576
|
continue;
|
|
339
577
|
}
|
|
340
578
|
|
|
341
579
|
const state = epicState(iid, glabRunFn, effectiveGlabRepo);
|
|
342
580
|
if (state === 'opened') {
|
|
343
|
-
skipped.push({ source: rel, reason: `epic-#${iid}-open`, iid });
|
|
581
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-open`, iid, via, ownership });
|
|
344
582
|
continue;
|
|
345
583
|
}
|
|
346
584
|
if (state === 'unknown') {
|
|
347
|
-
skipped.push({ source: rel, reason: `epic-#${iid}-state-unknown`, iid });
|
|
585
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-state-unknown`, iid, via, ownership });
|
|
348
586
|
warn(`archive-closed-prds: could not resolve state of Epic #${iid} for ${rel} — skipped.`);
|
|
349
587
|
continue;
|
|
350
588
|
}
|
|
@@ -364,11 +602,13 @@ export function main({
|
|
|
364
602
|
title: titleFromMarkdown(header),
|
|
365
603
|
});
|
|
366
604
|
} catch (err) {
|
|
367
|
-
skipped.push({ source: rel, reason: `archive-failed: ${err.message}`, iid });
|
|
605
|
+
skipped.push({ source: rel, reason: `archive-failed: ${err.message}`, iid, via, ownership });
|
|
368
606
|
warn(`archive-closed-prds: failed to archive ${rel}: ${err.message}`);
|
|
369
607
|
continue;
|
|
370
608
|
}
|
|
371
609
|
entry.iid = iid;
|
|
610
|
+
entry.via = via;
|
|
611
|
+
entry.ownership = ownership;
|
|
372
612
|
|
|
373
613
|
if (!isDryRun) {
|
|
374
614
|
const rm = gitRunFn(['-C', root, 'rm', '--', rel]);
|
package/scripts/autopilot.mjs
CHANGED
|
@@ -230,7 +230,12 @@ async function modeSelector() {
|
|
|
230
230
|
// `backlogLimit` is intentionally NOT passed: the window default lives once,
|
|
231
231
|
// in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`). The hand-written 50 that
|
|
232
232
|
// stood here under-read this repo's own backlog and hid its critical issues.
|
|
233
|
+
// `repoRoot` is passed EXPLICITLY (#1071) rather than left to the helper's
|
|
234
|
+
// cwd default: this driver already reads learnings.jsonl and sessions.jsonl
|
|
235
|
+
// relative to the invocation directory, and an implicit default is the seam
|
|
236
|
+
// where those two roots silently drift apart.
|
|
233
237
|
const signals = await buildLiveSignals({
|
|
238
|
+
repoRoot: process.cwd(),
|
|
234
239
|
learnings: surfacedLearnings,
|
|
235
240
|
});
|
|
236
241
|
return selectMode(signals);
|