session-orchestrator 3.21.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 +11 -2
- 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/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- 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 +269 -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/memory-proposal-collector.md +6 -4
- 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/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- 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 +7 -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 +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- 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 +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- 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-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- 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 +3 -1
- package/pi/prompts/eli5.md +12 -0
- 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/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- 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/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 +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- 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/dispatcher/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- 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/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- 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/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- 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 +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- 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/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 +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- 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-auq-clarity.mjs +274 -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 +59 -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-rules.mjs +153 -9
- 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-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- 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/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- 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/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- 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 +96 -48
- 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/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- 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 +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- 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
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
|
|
10
|
+
import { redactUrlCredentials } from '../vcs-repo-spec.mjs';
|
|
11
|
+
import { isValidRepoPath } from './manifest.mjs';
|
|
12
|
+
|
|
10
13
|
let _verbose = false;
|
|
11
14
|
|
|
12
15
|
/** Enable verbose stderr logging. */
|
|
@@ -38,7 +41,7 @@ export function assertGlabExists(dieFn) {
|
|
|
38
41
|
* Run a glab command, return { ok, stdout, stderr }.
|
|
39
42
|
*
|
|
40
43
|
* Host-pinning (#872): deliberately ambient — this module runs instance-wide
|
|
41
|
-
* queries (
|
|
44
|
+
* queries (`glab api groups/<group>/projects`) that are not scoped to a single
|
|
42
45
|
* repo/project, so there is no single `-R`/`--repo` spec to pin. If a
|
|
43
46
|
* caller ever needs single-repo host-pinning here, use `--hostname` (the
|
|
44
47
|
* flag `glab api` and instance-wide subcommands accept), NOT `-R`/`--repo` —
|
|
@@ -46,79 +49,129 @@ export function assertGlabExists(dieFn) {
|
|
|
46
49
|
* `resolveRepoHost` (`--hostname`) contract this repo already established.
|
|
47
50
|
*/
|
|
48
51
|
export function glabRun(glabArgs) {
|
|
49
|
-
vlog(`glab ${glabArgs.join(' ')}`);
|
|
52
|
+
vlog(`glab ${redactUrlCredentials(glabArgs.join(' '))}`);
|
|
50
53
|
const result = spawnSync('glab', glabArgs, {
|
|
51
54
|
encoding: 'utf8',
|
|
52
55
|
maxBuffer: 10 * 1024 * 1024,
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
if (result.error) {
|
|
56
|
-
return { ok: false, stdout: '', stderr: result.error.message };
|
|
59
|
+
return { ok: false, stdout: '', stderr: redactUrlCredentials(result.error.message) };
|
|
57
60
|
}
|
|
58
61
|
return {
|
|
59
62
|
ok: result.status === 0,
|
|
60
63
|
stdout: result.stdout || '',
|
|
61
|
-
stderr: result.stderr || '',
|
|
64
|
+
stderr: redactUrlCredentials(result.stderr || ''),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const PROJECT_VISIBILITIES = new Set(['private', 'internal', 'public']);
|
|
69
|
+
const ISO_TIMESTAMP = /^(?<year>[1-9]\d{3})-(?<month>\d{2})-(?<day>\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* GitLab returns `created_at` as an ISO-8601 timestamp. Validate the calendar
|
|
73
|
+
* components separately because Date normalizes impossible dates such as Feb 30.
|
|
74
|
+
*/
|
|
75
|
+
function isValidCreatedAt(value) {
|
|
76
|
+
if (typeof value !== 'string') return false;
|
|
77
|
+
|
|
78
|
+
const match = ISO_TIMESTAMP.exec(value);
|
|
79
|
+
if (!match?.groups) return false;
|
|
80
|
+
|
|
81
|
+
const { year, month, day } = match.groups;
|
|
82
|
+
const calendarDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
83
|
+
return (
|
|
84
|
+
calendarDate.getUTCFullYear() === Number(year) &&
|
|
85
|
+
calendarDate.getUTCMonth() === Number(month) - 1 &&
|
|
86
|
+
calendarDate.getUTCDate() === Number(day)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reduce a GitLab project response to the fields vault-backfill consumes.
|
|
92
|
+
*
|
|
93
|
+
* The group API with `simple=true` still includes presentation and URL fields.
|
|
94
|
+
* Keeping only this projection prevents those opaque project objects from
|
|
95
|
+
* flowing into logs or downstream actions.
|
|
96
|
+
*/
|
|
97
|
+
function normalizeRepo(project) {
|
|
98
|
+
return {
|
|
99
|
+
id: project.id,
|
|
100
|
+
path: project.path_with_namespace ?? project.path,
|
|
101
|
+
visibility: project.visibility === undefined ? 'private' : project.visibility,
|
|
102
|
+
createdAt: project.created_at === undefined ? '' : project.created_at.slice(0, 10),
|
|
62
103
|
};
|
|
63
104
|
}
|
|
64
105
|
|
|
106
|
+
function isSimpleProject(project) {
|
|
107
|
+
const path = project?.path_with_namespace ?? project?.path;
|
|
108
|
+
return (
|
|
109
|
+
project !== null &&
|
|
110
|
+
typeof project === 'object' &&
|
|
111
|
+
!Array.isArray(project) &&
|
|
112
|
+
Number.isInteger(project.id) &&
|
|
113
|
+
project.id > 0 &&
|
|
114
|
+
isValidRepoPath(path) &&
|
|
115
|
+
(project.visibility === undefined || PROJECT_VISIBILITIES.has(project.visibility)) &&
|
|
116
|
+
(project.created_at === undefined || isValidCreatedAt(project.created_at))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
65
120
|
/**
|
|
66
|
-
* Parse `glab
|
|
67
|
-
*
|
|
121
|
+
* Parse `glab api --paginate --slurp` output (a project array or page arrays).
|
|
122
|
+
*
|
|
123
|
+
* Returns an array of { id, path, visibility, createdAt }, or null when an
|
|
124
|
+
* exit-zero response does not fully match the simple-project list contract.
|
|
68
125
|
*/
|
|
69
126
|
export function parseRepoList(stdout) {
|
|
70
127
|
const trimmed = stdout.trim();
|
|
71
|
-
if (!trimmed) return
|
|
128
|
+
if (!trimmed) return null;
|
|
72
129
|
|
|
130
|
+
let data;
|
|
73
131
|
try {
|
|
74
|
-
|
|
75
|
-
if (!Array.isArray(data)) return [];
|
|
76
|
-
return data.map((r) => ({
|
|
77
|
-
id: r.id ?? 0,
|
|
78
|
-
path: r.path_with_namespace ?? r.path ?? '',
|
|
79
|
-
name: r.name ?? '',
|
|
80
|
-
visibility: r.visibility ?? 'private',
|
|
81
|
-
createdAt: (r.created_at ?? '').slice(0, 10),
|
|
82
|
-
}));
|
|
132
|
+
data = JSON.parse(trimmed);
|
|
83
133
|
} catch {
|
|
84
|
-
|
|
85
|
-
const repos = [];
|
|
86
|
-
for (const line of trimmed.split('\n')) {
|
|
87
|
-
const l = line.trim();
|
|
88
|
-
if (!l) continue;
|
|
89
|
-
try {
|
|
90
|
-
const r = JSON.parse(l);
|
|
91
|
-
repos.push({
|
|
92
|
-
id: r.id ?? 0,
|
|
93
|
-
path: r.path_with_namespace ?? r.path ?? '',
|
|
94
|
-
name: r.name ?? '',
|
|
95
|
-
visibility: r.visibility ?? 'private',
|
|
96
|
-
createdAt: (r.created_at ?? '').slice(0, 10),
|
|
97
|
-
});
|
|
98
|
-
} catch {
|
|
99
|
-
// skip malformed line
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return repos;
|
|
134
|
+
return null;
|
|
103
135
|
}
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(data)) return null;
|
|
138
|
+
if (data.length === 0) return [];
|
|
139
|
+
|
|
140
|
+
const projects = data.every(isSimpleProject)
|
|
141
|
+
? data
|
|
142
|
+
: data.every(Array.isArray)
|
|
143
|
+
? data.flat()
|
|
144
|
+
: null;
|
|
145
|
+
|
|
146
|
+
if (!projects || !projects.every(isSimpleProject)) return null;
|
|
147
|
+
return projects.map(normalizeRepo);
|
|
104
148
|
}
|
|
105
149
|
|
|
106
150
|
/**
|
|
107
151
|
* List all repos in a GitLab group. Returns repo array or null on API error.
|
|
108
152
|
*/
|
|
109
153
|
export function listGroupRepos(group) {
|
|
154
|
+
const encodedGroup = encodeURIComponent(group);
|
|
110
155
|
const { ok, stdout, stderr } = glabRun([
|
|
111
|
-
'
|
|
156
|
+
'api', `groups/${encodedGroup}/projects?simple=true&per_page=100`, '--paginate', '--slurp',
|
|
112
157
|
]);
|
|
113
158
|
|
|
114
159
|
if (!ok) {
|
|
115
160
|
process.stderr.write(
|
|
116
|
-
`[vault-backfill] WARN: glab
|
|
161
|
+
`[vault-backfill] WARN: glab api groups/<group>/projects failed for group '${group}': ${stderr.trim()}\n`,
|
|
162
|
+
);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const repos = parseRepoList(stdout);
|
|
167
|
+
if (repos === null) {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
`[vault-backfill] WARN: glab api groups/<group>/projects returned an unexpected project-list response shape for group '${group}'\n`,
|
|
117
170
|
);
|
|
118
171
|
return null;
|
|
119
172
|
}
|
|
120
173
|
|
|
121
|
-
return
|
|
174
|
+
return repos;
|
|
122
175
|
}
|
|
123
176
|
|
|
124
177
|
/**
|
|
@@ -150,23 +203,3 @@ export function checkVaultYaml(repoPath) {
|
|
|
150
203
|
);
|
|
151
204
|
return 'error';
|
|
152
205
|
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Fetch repo owner via glab API. Returns username or 'unknown' on failure.
|
|
156
|
-
*/
|
|
157
|
-
export function fetchRepoOwner(repoId) {
|
|
158
|
-
const { ok, stdout } = glabRun(['api', `projects/${repoId}`]);
|
|
159
|
-
if (!ok) return 'unknown';
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const data = JSON.parse(stdout);
|
|
163
|
-
return (
|
|
164
|
-
data?.namespace?.path ||
|
|
165
|
-
data?.owner?.username ||
|
|
166
|
-
data?.namespace?.name ||
|
|
167
|
-
'unknown'
|
|
168
|
-
);
|
|
169
|
-
} catch {
|
|
170
|
-
return 'unknown';
|
|
171
|
-
}
|
|
172
|
-
}
|
|
@@ -7,8 +7,34 @@
|
|
|
7
7
|
|
|
8
8
|
const VALID_TIERS = new Set(['top', 'active', 'archived']);
|
|
9
9
|
const VALID_VISIBILITIES = new Set(['public', 'internal', 'private']);
|
|
10
|
+
const GITLAB_PATH_SEGMENT_RE = /^[A-Za-z0-9_.-]+$/;
|
|
10
11
|
export const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Return whether a value is a canonical GitLab namespace/project path.
|
|
15
|
+
*
|
|
16
|
+
* GitLab project paths have at least one namespace and one project segment.
|
|
17
|
+
* Each segment uses the slug character set; dot segments remain excluded so
|
|
18
|
+
* the result is safe to use as a path below a controlled staging directory.
|
|
19
|
+
*
|
|
20
|
+
* @param {unknown} value
|
|
21
|
+
* @returns {value is string}
|
|
22
|
+
*/
|
|
23
|
+
export function isValidRepoPath(value) {
|
|
24
|
+
if (typeof value !== 'string') return false;
|
|
25
|
+
|
|
26
|
+
const segments = value.split('/');
|
|
27
|
+
return (
|
|
28
|
+
segments.length >= 2 &&
|
|
29
|
+
segments.every(
|
|
30
|
+
(segment) =>
|
|
31
|
+
segment !== '.' &&
|
|
32
|
+
segment !== '..' &&
|
|
33
|
+
GITLAB_PATH_SEGMENT_RE.test(segment),
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Validate and normalise a manifest object.
|
|
14
40
|
* Calls dieFn(1, ...) on any schema violation.
|
|
@@ -34,14 +60,8 @@ export function validateManifest(raw, dieFn) {
|
|
|
34
60
|
const prefix = `manifest.repos[${i}]`;
|
|
35
61
|
|
|
36
62
|
if (typeof entry.id !== 'number') dieFn(1, `${prefix}.id must be a number`);
|
|
37
|
-
if (
|
|
38
|
-
dieFn(1, `${prefix}.path must be a
|
|
39
|
-
}
|
|
40
|
-
if (entry.path.startsWith('/') || entry.path.split('/').includes('..')) {
|
|
41
|
-
dieFn(
|
|
42
|
-
1,
|
|
43
|
-
`${prefix}.path '${entry.path}' is invalid — must be a relative repo path (no leading '/' or '..' segments)`,
|
|
44
|
-
);
|
|
63
|
+
if (!isValidRepoPath(entry.path)) {
|
|
64
|
+
dieFn(1, `${prefix}.path must be a valid GitLab namespace/project path`);
|
|
45
65
|
}
|
|
46
66
|
if (typeof entry.slug !== 'string') dieFn(1, `${prefix}.slug must be a string`);
|
|
47
67
|
if (!SLUG_RE.test(entry.slug)) {
|
|
@@ -14,6 +14,7 @@ import { isRealSession } from '../session-schema/filters.mjs';
|
|
|
14
14
|
import { resolveRepoNamespace } from './namespace.mjs';
|
|
15
15
|
import { detectLearningSchema, normalizeLearningEntry, generateLearningNote, generateLearningNoteV2 } from './render-learnings.mjs';
|
|
16
16
|
import { detectSessionSchema, normalizeSessionEntry, generateSessionNote, generateSessionNoteV2, generateSessionNoteV3 } from './render-sessions.mjs';
|
|
17
|
+
import { emitMirrorEvent } from './telemetry.mjs';
|
|
17
18
|
|
|
18
19
|
const GENERATOR_MARKER = 'session-orchestrator-vault-mirror@1';
|
|
19
20
|
|
|
@@ -256,8 +257,33 @@ export { deriveRepo } from './namespace.mjs';
|
|
|
256
257
|
* @param {object} [opts.meta] — optional extra fields merged into the emitted JSON
|
|
257
258
|
* (used for quality-gate skips to carry a `reason` field). Callers that omit
|
|
258
259
|
* `meta` get the base JSON shape unchanged.
|
|
260
|
+
* @param {number} [opts.line] — 1-based JSONL line number. When FINITE, this
|
|
261
|
+
* entry also gets one `orchestrator.vault.mirror_completed` ledger record
|
|
262
|
+
* (#1147) — UNLESS `action` is `skipped-noop`, whose per-entry record is
|
|
263
|
+
* suppressed as ledger flood and reported only in the run-level roll-up
|
|
264
|
+
* (#1151; see the gate below). Omitting `line` keeps the stdout-only
|
|
265
|
+
* behaviour, which is what the direct unit tests of this function exercise.
|
|
266
|
+
* @param {boolean} [opts.dryRun] — the run's dry-run flag, for telemetry only.
|
|
267
|
+
* @param {string} [opts.skipClass] — `validation` | `mapper-crash`; telemetry only.
|
|
268
|
+
* @param {string} [opts.reason] — explicit telemetry reason. Defaults to
|
|
269
|
+
* `meta.reason` when that exists, so the quality-gate strings
|
|
270
|
+
* (`confidence:X < min:Y` / `narrative:N < min:M` / `status:…`) are REUSED
|
|
271
|
+
* rather than recomputed — recomputing is how one fact becomes two copies.
|
|
272
|
+
* @returns {Promise<string>} the `action` string, so the CLI's main loop can
|
|
273
|
+
* tally a run-level denominator without a second census of these call sites.
|
|
259
274
|
*/
|
|
260
|
-
export function emitAction({
|
|
275
|
+
export async function emitAction({
|
|
276
|
+
action,
|
|
277
|
+
path,
|
|
278
|
+
kind,
|
|
279
|
+
id,
|
|
280
|
+
vaultDir,
|
|
281
|
+
meta,
|
|
282
|
+
line,
|
|
283
|
+
dryRun,
|
|
284
|
+
skipClass,
|
|
285
|
+
reason,
|
|
286
|
+
}) {
|
|
261
287
|
let rel;
|
|
262
288
|
if (path === null || path === undefined) {
|
|
263
289
|
rel = null;
|
|
@@ -271,7 +297,59 @@ export function emitAction({ action, path, kind, id, vaultDir, meta }) {
|
|
|
271
297
|
if (meta && typeof meta === 'object') {
|
|
272
298
|
Object.assign(payload, meta);
|
|
273
299
|
}
|
|
300
|
+
// The stdout JSON-per-entry protocol is the CONTRACT other callers parse —
|
|
301
|
+
// it stays byte-identical; the ledger record below is purely additive.
|
|
274
302
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
303
|
+
|
|
304
|
+
// Per-entry ledger record for every action EXCEPT `skipped-noop` (#1151).
|
|
305
|
+
//
|
|
306
|
+
// `skipped-noop` is the STEADY STATE, not an event: on a populated vault
|
|
307
|
+
// nearly every record is already mirrored, so one `--kind session` run over
|
|
308
|
+
// this repo's ~276-record sessions ledger wrote ~276 per-entry records that
|
|
309
|
+
// between them said "nothing happened" — burying every other event class in
|
|
310
|
+
// the same file. No information is lost by dropping them: `finishRun()` in
|
|
311
|
+
// `scripts/vault-mirror.mjs` counts this class into the run event's `skipped`
|
|
312
|
+
// total AND names it in `action_breakdown['skipped-noop']`, so the noop COUNT
|
|
313
|
+
// stays measured per run and the gate stays falsifiable (HR-105).
|
|
314
|
+
//
|
|
315
|
+
// BV-004 ceiling: what this drops is the per-LINE locator for noops — the
|
|
316
|
+
// ledger can still say HOW MANY records were unchanged, never WHICH. Revisit
|
|
317
|
+
// if a consumer ever needs to name them (a staleness audit over unchanged
|
|
318
|
+
// notes would); reinstate it behind an opt-in flag then, not unconditionally.
|
|
319
|
+
if (Number.isFinite(line) && action !== 'skipped-noop') {
|
|
320
|
+
const telemetryReason =
|
|
321
|
+
reason ?? (meta && typeof meta.reason === 'string' ? meta.reason : undefined);
|
|
322
|
+
await emitMirrorEvent({
|
|
323
|
+
action,
|
|
324
|
+
kind,
|
|
325
|
+
line,
|
|
326
|
+
recordId: id,
|
|
327
|
+
// The VAULT-RELATIVE path (`rel`), never the absolute one: the ledger
|
|
328
|
+
// record can travel over the optional Clank webhook unredacted.
|
|
329
|
+
path: rel,
|
|
330
|
+
skipClass,
|
|
331
|
+
reason: telemetryReason,
|
|
332
|
+
dryRun,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return action;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Per-entry stdout + telemetry wrapper used by {@link processLearning} and
|
|
341
|
+
* {@link processSession}. Threads the line number, kind, vaultDir and dry-run
|
|
342
|
+
* flag out of the processor `ctx` so the 18 call sites do not repeat them.
|
|
343
|
+
*
|
|
344
|
+
* @param {number} lineNum — 1-based JSONL line number of the entry.
|
|
345
|
+
* @param {object} ctx — processor context (`vaultDir`, `dryRun`, `kind`).
|
|
346
|
+
* @param {object} opts — the remaining {@link emitAction} fields (`action`,
|
|
347
|
+
* `path`, `id`, and optionally `meta`).
|
|
348
|
+
* @returns {Promise<string>} the `action` string.
|
|
349
|
+
*/
|
|
350
|
+
function emitEntryAction(lineNum, ctx, opts) {
|
|
351
|
+
const { vaultDir, dryRun, kind } = ctx;
|
|
352
|
+
return emitAction({ ...opts, vaultDir, kind, line: lineNum, dryRun });
|
|
275
353
|
}
|
|
276
354
|
|
|
277
355
|
// ── Secret masking (#974) — THE choke-point ───────────────────────────────────
|
|
@@ -447,7 +525,7 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
447
525
|
const {
|
|
448
526
|
vaultDir,
|
|
449
527
|
dryRun,
|
|
450
|
-
kind,
|
|
528
|
+
kind: _kind, // threaded to emitAction via ctx by emitEntryAction (#1147); kept for ctx symmetry
|
|
451
529
|
force = false,
|
|
452
530
|
qualityMinConfidence = 0.5,
|
|
453
531
|
qualityMinNarrativeChars: _qualityMinNarrativeChars = 400, // unused for learnings; kept for ctx symmetry
|
|
@@ -531,15 +609,12 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
531
609
|
// Missing/non-numeric confidence is treated as 1.0 (legacy entries pass).
|
|
532
610
|
const learningConfidence = typeof entry.confidence === 'number' ? entry.confidence : 1.0;
|
|
533
611
|
if (learningConfidence < qualityMinConfidence) {
|
|
534
|
-
|
|
612
|
+
return emitEntryAction(_lineNum, ctx, {
|
|
535
613
|
action: 'skipped-quality-low',
|
|
536
614
|
path: null,
|
|
537
|
-
kind,
|
|
538
615
|
id: entryId,
|
|
539
|
-
vaultDir,
|
|
540
616
|
meta: { reason: `confidence:${learningConfidence} < min:${qualityMinConfidence}` },
|
|
541
617
|
});
|
|
542
|
-
return;
|
|
543
618
|
}
|
|
544
619
|
|
|
545
620
|
// #660: namespace new writes under a per-repo subdirectory.
|
|
@@ -572,8 +647,7 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
572
647
|
// Render the candidate and compare canonical fields before deciding to skip.
|
|
573
648
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
574
649
|
if (learningContentMatches(legacyContent, candidateContent)) {
|
|
575
|
-
|
|
576
|
-
return;
|
|
650
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: slug });
|
|
577
651
|
}
|
|
578
652
|
// Content differs — fall through to write into the namespaced path.
|
|
579
653
|
}
|
|
@@ -588,15 +662,13 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
588
662
|
if (!fm || !fm['_generator']) {
|
|
589
663
|
// Hand-written: skip
|
|
590
664
|
process.stderr.write(`SKIP hand-written: ${targetPath}\n`);
|
|
591
|
-
|
|
592
|
-
return;
|
|
665
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: entryId });
|
|
593
666
|
}
|
|
594
667
|
|
|
595
668
|
if (fm['_generator'] !== GENERATOR_MARKER) {
|
|
596
669
|
// Different generator — treat as hand-written to be safe
|
|
597
670
|
process.stderr.write(`SKIP unknown generator: ${targetPath}\n`);
|
|
598
|
-
|
|
599
|
-
return;
|
|
671
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: entryId });
|
|
600
672
|
}
|
|
601
673
|
|
|
602
674
|
if (fm['id'] !== slug) {
|
|
@@ -611,16 +683,14 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
611
683
|
const disambigFm = parseFrontmatter(disambigContent);
|
|
612
684
|
if (!disambigFm || !disambigFm['_generator']) {
|
|
613
685
|
process.stderr.write(`SKIP hand-written (disambig): ${targetPath}\n`);
|
|
614
|
-
|
|
615
|
-
return;
|
|
686
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: entryId });
|
|
616
687
|
}
|
|
617
688
|
// Check updated advancement; if date has not advanced, also diff content.
|
|
618
689
|
const entryUpdated = toDate(dateSource);
|
|
619
690
|
if (disambigFm['updated'] && disambigFm['updated'] >= entryUpdated) {
|
|
620
691
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
621
692
|
if (learningContentMatches(disambigContent, candidateContent)) {
|
|
622
|
-
|
|
623
|
-
return;
|
|
693
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: disambigSlug });
|
|
624
694
|
}
|
|
625
695
|
// Content differs — fall through to write.
|
|
626
696
|
}
|
|
@@ -628,8 +698,7 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
628
698
|
|
|
629
699
|
const content = generator(entry, slug, generatorOpts);
|
|
630
700
|
if (!dryRun) writeFileSync(targetPath, content, 'utf8');
|
|
631
|
-
|
|
632
|
-
return;
|
|
701
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-collision-resolved', path: targetPath, id: slug });
|
|
633
702
|
}
|
|
634
703
|
|
|
635
704
|
// Same id: check if updated would advance (unless --force overrides).
|
|
@@ -639,8 +708,7 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
639
708
|
if (!force && fm['updated'] && fm['updated'] >= entryUpdated) {
|
|
640
709
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
641
710
|
if (learningContentMatches(existingContent, candidateContent)) {
|
|
642
|
-
|
|
643
|
-
return;
|
|
711
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: slug });
|
|
644
712
|
}
|
|
645
713
|
// Content differs — fall through to overwrite (same path as date-advance branch).
|
|
646
714
|
}
|
|
@@ -648,21 +716,20 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
648
716
|
// Overwrite with advanced updated date (or forced re-render)
|
|
649
717
|
const content = generator(entry, slug, generatorOpts);
|
|
650
718
|
if (!dryRun) writeFileSync(targetPath, content, 'utf8');
|
|
651
|
-
|
|
652
|
-
return;
|
|
719
|
+
return emitEntryAction(_lineNum, ctx, { action: 'updated', path: targetPath, id: slug });
|
|
653
720
|
}
|
|
654
721
|
|
|
655
722
|
// File does not exist — create
|
|
656
723
|
const content = generator(entry, slug, generatorOpts);
|
|
657
724
|
if (!dryRun) writeFileSync(targetPath, content, 'utf8');
|
|
658
|
-
|
|
725
|
+
return emitEntryAction(_lineNum, ctx, { action: 'created', path: targetPath, id: slug });
|
|
659
726
|
}
|
|
660
727
|
|
|
661
728
|
export async function processSession(rawEntry, _lineNum, ctx) {
|
|
662
729
|
const {
|
|
663
730
|
vaultDir,
|
|
664
731
|
dryRun,
|
|
665
|
-
kind,
|
|
732
|
+
kind: _kind, // threaded to emitAction via ctx by emitEntryAction (#1147); kept for ctx symmetry
|
|
666
733
|
force = false,
|
|
667
734
|
qualityMinNarrativeChars = 400,
|
|
668
735
|
qualityMinConfidence: _qualityMinConfidence = 0.5, // unused for sessions; kept for ctx symmetry
|
|
@@ -716,15 +783,12 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
716
783
|
// status from the vault; the mapping keeps every OTHER status honest, and
|
|
717
784
|
// guards the generators' other entry point (the render.mjs barrel).
|
|
718
785
|
if (!isRealSession(entry)) {
|
|
719
|
-
|
|
786
|
+
return emitEntryAction(_lineNum, ctx, {
|
|
720
787
|
action: 'skipped-abandoned',
|
|
721
788
|
path: null,
|
|
722
|
-
kind,
|
|
723
789
|
id: session_id,
|
|
724
|
-
vaultDir,
|
|
725
790
|
meta: { reason: `status:${entry?.status}` },
|
|
726
791
|
});
|
|
727
|
-
return;
|
|
728
792
|
}
|
|
729
793
|
|
|
730
794
|
// #732: resolve the leak-guarded repo namespace ONCE per session, BEFORE the
|
|
@@ -752,15 +816,12 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
752
816
|
const narrativeBody = renderedBody.replace(/^---[\s\S]*?---/m, '').trim();
|
|
753
817
|
const narrativeChars = narrativeBody.length;
|
|
754
818
|
if (narrativeChars < qualityMinNarrativeChars) {
|
|
755
|
-
|
|
819
|
+
return emitEntryAction(_lineNum, ctx, {
|
|
756
820
|
action: 'skipped-quality-low',
|
|
757
821
|
path: null,
|
|
758
|
-
kind,
|
|
759
822
|
id: session_id,
|
|
760
|
-
vaultDir,
|
|
761
823
|
meta: { reason: `narrative:${narrativeChars} < min:${qualityMinNarrativeChars}` },
|
|
762
824
|
});
|
|
763
|
-
return;
|
|
764
825
|
}
|
|
765
826
|
|
|
766
827
|
// #660: namespace new writes under a per-repo subdirectory. repoNs was
|
|
@@ -786,8 +847,7 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
786
847
|
if (legacyFm && legacyFm['_generator'] === GENERATOR_MARKER && legacyFm['id'] === session_id) {
|
|
787
848
|
const entryUpdated = toDate(entry.completed_at);
|
|
788
849
|
if (!force && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
|
|
789
|
-
|
|
790
|
-
return;
|
|
850
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: session_id });
|
|
791
851
|
}
|
|
792
852
|
// Updated date would advance — fall through to write into the namespaced path.
|
|
793
853
|
}
|
|
@@ -800,31 +860,27 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
800
860
|
if (!fm || !fm['_generator']) {
|
|
801
861
|
// Hand-written: skip
|
|
802
862
|
process.stderr.write(`SKIP hand-written: ${targetPath}\n`);
|
|
803
|
-
|
|
804
|
-
return;
|
|
863
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: session_id });
|
|
805
864
|
}
|
|
806
865
|
|
|
807
866
|
if (fm['_generator'] !== GENERATOR_MARKER) {
|
|
808
867
|
process.stderr.write(`SKIP unknown generator: ${targetPath}\n`);
|
|
809
|
-
|
|
810
|
-
return;
|
|
868
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: session_id });
|
|
811
869
|
}
|
|
812
870
|
|
|
813
871
|
// Same generator: check id and updated
|
|
814
872
|
if (fm['id'] === session_id) {
|
|
815
873
|
const entryUpdated = toDate(entry.completed_at);
|
|
816
874
|
if (!force && fm['updated'] && fm['updated'] >= entryUpdated) {
|
|
817
|
-
|
|
818
|
-
return;
|
|
875
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: session_id });
|
|
819
876
|
}
|
|
820
877
|
if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
|
|
821
|
-
|
|
822
|
-
return;
|
|
878
|
+
return emitEntryAction(_lineNum, ctx, { action: 'updated', path: targetPath, id: session_id });
|
|
823
879
|
}
|
|
824
880
|
}
|
|
825
881
|
|
|
826
882
|
// File does not exist — create. Reuse the rendered body computed during the
|
|
827
883
|
// quality-gate check (avoids a second generator invocation).
|
|
828
884
|
if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
|
|
829
|
-
|
|
885
|
+
return emitEntryAction(_lineNum, ctx, { action: 'created', path: targetPath, id: session_id });
|
|
830
886
|
}
|