@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -61,6 +61,13 @@ const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
|
61
61
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
62
62
|
const verificationMod = require("./verification.cjs");
|
|
63
63
|
const { isPhaseComplete } = verificationMod;
|
|
64
|
+
// #4129: the single owner of "count the ROADMAP's milestone Complete rows"
|
|
65
|
+
// (phase-lifecycle.cts) — reused for the completed-phases numerator floor so
|
|
66
|
+
// this scan cannot grow a second ROADMAP parser. Pure computation module (no
|
|
67
|
+
// I/O), so it introduces no cycle on this path.
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
const phaseLifecycleMod = require("./phase-lifecycle.cjs");
|
|
70
|
+
const { deriveProgressFromRoadmap } = phaseLifecycleMod;
|
|
64
71
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
72
|
const planningScopeMod = require("./planning-scope.cjs");
|
|
66
73
|
const { SCOPE } = planningScopeMod;
|
|
@@ -286,7 +293,7 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
286
293
|
// Try to find markdown section or field
|
|
287
294
|
const fieldEscaped = (0, pattern_cjs_1.escapeRegex)(section);
|
|
288
295
|
// Check for **field:** value (bold format)
|
|
289
|
-
const boldPattern = new RegExp(
|
|
296
|
+
const boldPattern = new RegExp(`^[ \\t]*\\*\\*${fieldEscaped}:\\*\\*[ \\t]*(.*)`, 'im');
|
|
290
297
|
const boldMatch = content.match(boldPattern);
|
|
291
298
|
if (boldMatch) {
|
|
292
299
|
output({ [section]: boldMatch[1].trim() }, raw, boldMatch[1].trim());
|
|
@@ -314,13 +321,10 @@ function readTextArgOrFile(cwd, value, filePath, label) {
|
|
|
314
321
|
return value;
|
|
315
322
|
// Path traversal guard: ensure file resolves within project directory
|
|
316
323
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method
|
|
317
|
-
const {
|
|
318
|
-
const
|
|
319
|
-
if (!pathCheck.safe) {
|
|
320
|
-
throw new Error(`${label} path rejected: ${pathCheck.error}`);
|
|
321
|
-
}
|
|
324
|
+
const { assertWithinRoot, PathAcceptance } = require('./security.cjs');
|
|
325
|
+
const contained = assertWithinRoot(filePath, cwd, `${label} path`, PathAcceptance.AbsoluteInsideRoot);
|
|
322
326
|
try {
|
|
323
|
-
return node_fs_1.default.readFileSync(
|
|
327
|
+
return node_fs_1.default.readFileSync(contained, 'utf-8').trimEnd();
|
|
324
328
|
}
|
|
325
329
|
catch {
|
|
326
330
|
throw new Error(`${label} file not found: ${filePath}`);
|
|
@@ -489,7 +493,31 @@ function cmdStateUpdate(cwd, field, value) {
|
|
|
489
493
|
// (#3345's direction) — reported separately from `updated` because this
|
|
490
494
|
// command's contract is a single-field boolean, not a per-field array.
|
|
491
495
|
const reconciled = reconcileReportedFields(statePath, preWriteState, updated ? [field] : [], divergedFields);
|
|
492
|
-
|
|
496
|
+
// #4488: `updateCore` itself already told us whether it matched the field
|
|
497
|
+
// (`updated`, captured above `readModifyWriteStateMd` runs it) — that is a
|
|
498
|
+
// real signal, not a guess. `reconcileReportedFields` answers a DIFFERENT
|
|
499
|
+
// question ("what changed on disk") and, per its own docstring, reports
|
|
500
|
+
// `[]` whenever `preWriteState.fm` is `undefined`. That happens in two
|
|
501
|
+
// known cases, both of which mean "no snapshot was ever captured", not
|
|
502
|
+
// "nothing happened": (a) `readModifyWriteStateMd`'s #948 no-op guard
|
|
503
|
+
// fires because the transform's output was byte-identical to the input —
|
|
504
|
+
// the requested value already equals what's on disk, so the field WAS
|
|
505
|
+
// found and there was simply nothing left to change; (b)
|
|
506
|
+
// `applyPostSyncPreservation`'s `isUnparseableFrontmatter` early return —
|
|
507
|
+
// the ORIGINAL frontmatter block was malformed, so preservation never
|
|
508
|
+
// runs, yet `readModifyWriteStateMd` still persists the transform's raw
|
|
509
|
+
// output via `platformWriteSync`. In neither case did preservation
|
|
510
|
+
// discard or rewrite what the transform wrote, so trusting the
|
|
511
|
+
// transform's own `updated` signal here is never a false positive.
|
|
512
|
+
// Collapsing either case into the same `false` as "field not found" is
|
|
513
|
+
// the #4488 bug — `explainUpdateFailure` then reports a message that is
|
|
514
|
+
// actively false (it tells the caller to add a line that is already
|
|
515
|
+
// there). Every other `false` origin (case-D fallback did not apply, or
|
|
516
|
+
// the transform genuinely found nothing) is unaffected: there
|
|
517
|
+
// `preWriteState.fm` is defined (a normal sync ran) or `updated` was
|
|
518
|
+
// already false before this line.
|
|
519
|
+
const noopBecauseAlreadyCorrect = updated && preWriteState.fm === undefined;
|
|
520
|
+
updated = reconciled.includes(field) || noopBecauseAlreadyCorrect;
|
|
493
521
|
const preserved = reconciled.filter((f) => f !== field);
|
|
494
522
|
if (updated) {
|
|
495
523
|
// #3699 case D: surfaced so a caller can tell "wrote the body source" from
|
|
@@ -1560,6 +1588,18 @@ function cmdStateResolveBlocker(cwd, text, raw) {
|
|
|
1560
1588
|
}
|
|
1561
1589
|
}
|
|
1562
1590
|
function cmdStateRecordSession(cwd, options, raw) {
|
|
1591
|
+
// #4186: a bare invocation is a usage error, not a heartbeat write. The
|
|
1592
|
+
// pre-#4186 handler accepted zero arguments and still refreshed
|
|
1593
|
+
// `Last session` / `Last Date` / `last_updated` — a caller probing the
|
|
1594
|
+
// command's signature (the way other subcommands encourage) silently
|
|
1595
|
+
// mutated STATE.md. Mirrors `state update`'s required-arg guard
|
|
1596
|
+
// (cmdStateUpdate: `error('field and value required for state update')`),
|
|
1597
|
+
// including its ordering: validation precedes the STATE.md existence
|
|
1598
|
+
// check. Either flag suffices — `--resume-file` alone carries an explicit
|
|
1599
|
+
// value the handler must persist.
|
|
1600
|
+
if (!options.stopped_at && (options.resume_file === undefined || options.resume_file === null)) {
|
|
1601
|
+
error('stopped-at or resume-file required for state record-session');
|
|
1602
|
+
}
|
|
1563
1603
|
const statePath = planningPaths(cwd).state;
|
|
1564
1604
|
if (!node_fs_1.default.existsSync(statePath)) {
|
|
1565
1605
|
output({ error: 'STATE.md not found' }, raw, undefined);
|
|
@@ -2512,7 +2552,10 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
|
|
|
2512
2552
|
// own comment on that field). Folding this consumer onto the raw
|
|
2513
2553
|
// summaries-met flag was the exact "consolidate two of three and
|
|
2514
2554
|
// leave the third" gap §7.4's forcing function rules out.
|
|
2515
|
-
|
|
2555
|
+
// #612: `phaseConvention` threaded so a bracket phase dir resolves
|
|
2556
|
+
// and scopes its verification report like its legacy twin — the
|
|
2557
|
+
// read-side half of the same thread cmdStateSync gets below.
|
|
2558
|
+
if (isPhaseComplete(phaseDir, { convention: phaseConvention }).value.complete)
|
|
2516
2559
|
diskCompletedPhases++;
|
|
2517
2560
|
}
|
|
2518
2561
|
// Count phase headings from ROADMAP — single source of truth for
|
|
@@ -2619,6 +2662,34 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
|
|
|
2619
2662
|
// write silently clobbered the three stored siblings with the
|
|
2620
2663
|
// under-scoped disk numbers.
|
|
2621
2664
|
const diskCountsWithheld = milestonedButUnbounded || roadmapAbsentWithAssertedMilestone;
|
|
2665
|
+
// #4129: floor the completed-phases numerator at the ROADMAP's own
|
|
2666
|
+
// milestone Complete-row count. The disk numerator counts ONLY
|
|
2667
|
+
// phase dirs whose *-VERIFICATION.md routes `passed` (isPhaseComplete,
|
|
2668
|
+
// #2957 disk-strict — the gate stays untouched), so a completed
|
|
2669
|
+
// phase whose verification reads `stale` (a SUMMARY committed or
|
|
2670
|
+
// edited after it, #2348 clean-commit-time clock) or `missing`
|
|
2671
|
+
// (pre-verification era, hand-flipped ROADMAP row) drops out of the
|
|
2672
|
+
// count forever — while every other surface (the ROADMAP row
|
|
2673
|
+
// `phase complete` just flipped, the body `Completed Phases` field
|
|
2674
|
+
// completePhaseCore derives from deriveProgressFromRoadmap) still
|
|
2675
|
+
// asserts the phase complete. max(disk, ROADMAP) keeps the disk
|
|
2676
|
+
// signal for gap detection (a verification-passed phase whose ROADMAP
|
|
2677
|
+
// row is not yet flipped still counts) while never UNDER-counting
|
|
2678
|
+
// what the ROADMAP asserts. Scoped exactly like the denominator:
|
|
2679
|
+
// the same milestone window (roadmapScope), the same
|
|
2680
|
+
// safeToUseRoadmapCount gate, and never under the #3354/#3573
|
|
2681
|
+
// withhold — a whole-document Complete-row count must not leak
|
|
2682
|
+
// through an untrustworthy scope. Reuses deriveProgressFromRoadmap
|
|
2683
|
+
// (phase-lifecycle.cts, the one owner of "read the Progress table")
|
|
2684
|
+
// — no second ROADMAP parser here. A ROADMAP without a canonical
|
|
2685
|
+
// `## Progress` table resolves no table → floor inert (disk count
|
|
2686
|
+
// stands), the owner's own answer to "what is countable".
|
|
2687
|
+
const roadmapCompletedPhases = roadmapScope !== null && safeToUseRoadmapCount && !diskCountsWithheld
|
|
2688
|
+
? deriveProgressFromRoadmap(roadmapScope).completedPhases
|
|
2689
|
+
: null;
|
|
2690
|
+
const flooredCompletedPhases = roadmapCompletedPhases !== null
|
|
2691
|
+
? Math.max(diskCompletedPhases, roadmapCompletedPhases)
|
|
2692
|
+
: diskCompletedPhases;
|
|
2622
2693
|
return {
|
|
2623
2694
|
// The two WITHHOLD shapes (#3354 milestoned-but-unbounded, #3573
|
|
2624
2695
|
// roadmap-absent-with-asserted-milestone) must be evaluated BEFORE
|
|
@@ -2629,7 +2700,7 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
|
|
|
2629
2700
|
? null
|
|
2630
2701
|
: (safeToUseRoadmapCount ? Math.max(phaseDirs.length, roadmapPhaseCount) : phaseDirs.length),
|
|
2631
2702
|
milestoneBounded,
|
|
2632
|
-
completedPhases: diskCountsWithheld ? null :
|
|
2703
|
+
completedPhases: diskCountsWithheld ? null : flooredCompletedPhases,
|
|
2633
2704
|
totalPlans: diskCountsWithheld ? null : diskTotalPlans,
|
|
2634
2705
|
completedPlans: diskCountsWithheld ? null : diskTotalSummaries,
|
|
2635
2706
|
phaseDirScope,
|
|
@@ -2722,19 +2793,19 @@ storedCompletedPhases, storedTotalPlans, storedCompletedPlans) {
|
|
|
2722
2793
|
progressPercent = parseInt(pctMatch[1], 10);
|
|
2723
2794
|
}
|
|
2724
2795
|
let normalizedStatus = (0, state_document_cjs_1.normalizeStateStatus)(status, pausedAt);
|
|
2725
|
-
// #3578:
|
|
2726
|
-
//
|
|
2727
|
-
//
|
|
2728
|
-
//
|
|
2729
|
-
// prose must never decide milestone-level
|
|
2730
|
-
// totalPhases / diskScope, already derived above
|
|
2731
|
-
// the authority on whether the MILESTONE is actually
|
|
2732
|
-
// when: (a) normalizeStateStatus actually landed on
|
|
2733
|
-
// raw prose is UNAMBIGUOUSLY phase-completion prose —
|
|
2734
|
-
// pattern below deliberately excludes "All phases complete"
|
|
2735
|
-
// phase token) and milestone-close prose like "v1.0 milestone
|
|
2736
|
-
// (no leading "phase"); and (c) the counters are trustworthy (a
|
|
2737
|
-
// disk scope, both counts are finite numbers, and a positive
|
|
2796
|
+
// #3578: the declared status vocabulary (#4186) recognizes
|
|
2797
|
+
// `Phase ${N} complete` (state.cts's own phase-completion write) and maps
|
|
2798
|
+
// it to `completed`, so the phase-completion prose still collapses to the
|
|
2799
|
+
// milestone-level status even when other phases remain open — this guard
|
|
2800
|
+
// demotes it back. Phase-level prose must never decide milestone-level
|
|
2801
|
+
// status — completedPhases / totalPhases / diskScope, already derived above
|
|
2802
|
+
// from a disk scan, are the authority on whether the MILESTONE is actually
|
|
2803
|
+
// done. Only override when: (a) normalizeStateStatus actually landed on
|
|
2804
|
+
// 'completed'; (b) the raw prose is UNAMBIGUOUSLY phase-completion prose —
|
|
2805
|
+
// the anchored pattern below deliberately excludes "All phases complete"
|
|
2806
|
+
// (no `\S+` phase token) and milestone-close prose like "v1.0 milestone
|
|
2807
|
+
// complete" (no leading "phase"); and (c) the counters are trustworthy (a
|
|
2808
|
+
// COMPLETE disk scope, both counts are finite numbers, and a positive
|
|
2738
2809
|
// denominator) and affirmatively disagree with 'completed'. In every
|
|
2739
2810
|
// other case normalizedStatus is left exactly as normalizeStateStatus
|
|
2740
2811
|
// returned it.
|
|
@@ -2996,6 +3067,75 @@ function readStoredTotalPlans(existingFm) {
|
|
|
2996
3067
|
function readStoredCompletedPlans(existingFm) {
|
|
2997
3068
|
return readStoredProgressCounter(existingFm, 'completed_plans');
|
|
2998
3069
|
}
|
|
3070
|
+
/**
|
|
3071
|
+
* #4129: is this authoritativeFm value a PARTIAL progress intent? The #2736
|
|
3072
|
+
* seam was string-only (names); #4129 extends it with one object direction —
|
|
3073
|
+
* the `progress` key carrying the sub-keys a transition resolved
|
|
3074
|
+
* authoritatively (completePhase's ROADMAP-derived completed_phases/percent).
|
|
3075
|
+
* Anything else keeps the seam's existing contract untouched.
|
|
3076
|
+
*/
|
|
3077
|
+
function isPartialProgressIntent(value) {
|
|
3078
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* #4129: merge a PARTIAL progress intent (see isPartialProgressIntent) into a
|
|
3082
|
+
* frontmatter object's `progress` block. Sub-keys are accepted only when they
|
|
3083
|
+
* are a declared `progress.*` row in FIELD_CLASSIFICATION — the single policy
|
|
3084
|
+
* source (ADR-3408 §8.5) decides which leaves exist; an intent may not invent
|
|
3085
|
+
* one. Returns whether anything changed.
|
|
3086
|
+
*
|
|
3087
|
+
* `completedOnlyRaise` (both application sites use it): completed counters
|
|
3088
|
+
* apply only when strictly greater than what is already in the block, so no
|
|
3089
|
+
* intent can LOWER a count another trustworthy signal already established —
|
|
3090
|
+
* at the pre-preservation site the disk derivation's own count (a
|
|
3091
|
+
* verification-passed phase whose ROADMAP row drifted behind), at the
|
|
3092
|
+
* post-preservation re-assert the #2969 monotonic property preservation just
|
|
3093
|
+
* enforced. `percent` follows its sibling: it is applied when a completed
|
|
3094
|
+
* counter moved this call (the intent percent was computed from the intent
|
|
3095
|
+
* counters and is coherent with them) or when the block has no percent to
|
|
3096
|
+
* lose (a repair, never a regression of an upstream withhold — the withhold
|
|
3097
|
+
* nulled percent upstream precisely so no write would re-assert one over
|
|
3098
|
+
* untrustworthy counts; here the intent's own counts ARE the trustworthy
|
|
3099
|
+
* source, the post-completion ROADMAP).
|
|
3100
|
+
*/
|
|
3101
|
+
function applyAuthoritativeProgressSubkeys(fm, intent, opts) {
|
|
3102
|
+
const current = fm['progress'];
|
|
3103
|
+
const base = isPartialProgressIntent(current)
|
|
3104
|
+
? { ...current }
|
|
3105
|
+
: {};
|
|
3106
|
+
let changed = false;
|
|
3107
|
+
let completedMoved = false;
|
|
3108
|
+
for (const [subkey, value] of Object.entries(intent)) {
|
|
3109
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
3110
|
+
continue;
|
|
3111
|
+
if (!getFieldClassification(`progress.${subkey}`))
|
|
3112
|
+
continue;
|
|
3113
|
+
const isCompletedCounter = subkey === 'completed_phases' || subkey === 'completed_plans';
|
|
3114
|
+
if (isCompletedCounter && opts.completedOnlyRaise) {
|
|
3115
|
+
const currentNum = (0, state_document_cjs_1.toFiniteNumber)(base[subkey]);
|
|
3116
|
+
if (currentNum !== null && currentNum >= value)
|
|
3117
|
+
continue;
|
|
3118
|
+
}
|
|
3119
|
+
if (isCompletedCounter && !Object.is(base[subkey], value))
|
|
3120
|
+
completedMoved = true;
|
|
3121
|
+
if (!Object.is(base[subkey], value)) {
|
|
3122
|
+
base[subkey] = value;
|
|
3123
|
+
changed = true;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
// percent: applied only when a completed counter moved (coherent with the
|
|
3127
|
+
// counters that just landed) or when no percent exists to contradict.
|
|
3128
|
+
const intentPercent = intent['percent'];
|
|
3129
|
+
if (typeof intentPercent === 'number' && Number.isFinite(intentPercent) && (completedMoved || (0, state_document_cjs_1.toFiniteNumber)(base['percent']) === null)) {
|
|
3130
|
+
if (!Object.is(base['percent'], intentPercent)) {
|
|
3131
|
+
base['percent'] = intentPercent;
|
|
3132
|
+
changed = true;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
if (changed)
|
|
3136
|
+
fm['progress'] = base;
|
|
3137
|
+
return changed;
|
|
3138
|
+
}
|
|
2999
3139
|
function syncStateFrontmatter(content, cwd, authoritativeFm, sanctionedPermanentEmptyFallback) {
|
|
3000
3140
|
// Read existing frontmatter BEFORE stripping — it may contain values
|
|
3001
3141
|
// that the body no longer has (e.g., Status field removed by an agent).
|
|
@@ -3201,11 +3341,22 @@ function syncStateFrontmatter(content, cwd, authoritativeFm, sanctionedPermanent
|
|
|
3201
3341
|
// parenthetical (`Closer-ruling measurement (D1a)` → `D1a`) — never runs
|
|
3202
3342
|
// the final word on a field the transition just resolved. The prose parser
|
|
3203
3343
|
// remains the fallback for genuinely unknown prose only.
|
|
3344
|
+
// #4129: the `progress` key carries a PARTIAL block (the object direction of
|
|
3345
|
+
// this seam — see applyAuthoritativeProgressSubkeys) for the same reason:
|
|
3346
|
+
// completePhase holds the POST-completion ROADMAP, and the disk scan this
|
|
3347
|
+
// function drives reads the PRE-completion one. The intent is applied as a
|
|
3348
|
+
// FLOOR here too (completedOnlyRaise): a derivation that already counted
|
|
3349
|
+
// MORE completed phases than the ROADMAP table asserts (verification-passed
|
|
3350
|
+
// phases whose table rows drifted behind) must not be lowered by the intent
|
|
3351
|
+
// — the two signals agree on direction (up), never on subtraction.
|
|
3204
3352
|
if (authoritativeFm) {
|
|
3205
3353
|
for (const [key, value] of Object.entries(authoritativeFm)) {
|
|
3206
3354
|
if (typeof value === 'string' && value.trim().length > 0) {
|
|
3207
3355
|
derivedFm[key] = value;
|
|
3208
3356
|
}
|
|
3357
|
+
else if (key === 'progress' && isPartialProgressIntent(value)) {
|
|
3358
|
+
applyAuthoritativeProgressSubkeys(derivedFm, value, { completedOnlyRaise: true });
|
|
3359
|
+
}
|
|
3209
3360
|
}
|
|
3210
3361
|
}
|
|
3211
3362
|
// #3257: propagate full-line frontmatter comments from the extracted source onto the
|
|
@@ -3782,6 +3933,10 @@ function applyPostSyncPreservation(originalContent, transformedContent, syncedCo
|
|
|
3782
3933
|
// (equal), so the #1695 restore fires and would put the stale pre-transition
|
|
3783
3934
|
// name back over the authoritative one. Intent beats both the prose
|
|
3784
3935
|
// re-derivation and the curated restore — the transition just resolved it.
|
|
3936
|
+
// #4129: for the `progress` key the re-assert is a FLOOR, not an override —
|
|
3937
|
+
// the #2969 monotonic property preservation just enforced (completed
|
|
3938
|
+
// counters never move down) must not be undone by the intent, so completed
|
|
3939
|
+
// sub-keys apply only-raise here (see applyAuthoritativeProgressSubkeys).
|
|
3785
3940
|
let authoritativeReasserted = false;
|
|
3786
3941
|
if (authoritativeFm) {
|
|
3787
3942
|
for (const [key, value] of Object.entries(authoritativeFm)) {
|
|
@@ -3789,6 +3944,11 @@ function applyPostSyncPreservation(originalContent, transformedContent, syncedCo
|
|
|
3789
3944
|
preservation.postFm[key] = value;
|
|
3790
3945
|
authoritativeReasserted = true;
|
|
3791
3946
|
}
|
|
3947
|
+
else if (key === 'progress' && isPartialProgressIntent(value)) {
|
|
3948
|
+
if (applyAuthoritativeProgressSubkeys(preservation.postFm, value, { completedOnlyRaise: true })) {
|
|
3949
|
+
authoritativeReasserted = true;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3792
3952
|
}
|
|
3793
3953
|
}
|
|
3794
3954
|
let finalContent = syncedContent;
|
|
@@ -4454,6 +4614,26 @@ function cmdStateJson(cwd, raw) {
|
|
|
4454
4614
|
* Fixes: #1102 (plan counts), #1103 (status/last_activity), #1104 (body text).
|
|
4455
4615
|
*/
|
|
4456
4616
|
function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
4617
|
+
// #4138: `--phase` is this verb's one required argument, and an invocation
|
|
4618
|
+
// that names no phase must fail closed BEFORE any read-modify-write runs —
|
|
4619
|
+
// previously the missing flag flowed through as null and the transition
|
|
4620
|
+
// serialised `String(null)` into the body (`Phase: null — EXECUTING`,
|
|
4621
|
+
// `Status: Executing Phase null`, `last_activity_desc: Phase null execution
|
|
4622
|
+
// started`) while the post-sync frontmatter rebuild dropped current_phase /
|
|
4623
|
+
// current_phase_name entirely, so a single argument-less call un-set the
|
|
4624
|
+
// phase identity. The guard mirrors the sibling usage errors that already
|
|
4625
|
+
// exit non-zero (`state update`'s "field and value required", the router's
|
|
4626
|
+
// "unexpected positional argument" / "Invalid --plans value"), NOT
|
|
4627
|
+
// `cmdStateMilestoneSwitch`'s `output({error})` form, which exits 0 — the
|
|
4628
|
+
// issue's Expected is explicit: "Exit non-zero with a usage message and
|
|
4629
|
+
// write nothing." Empty and whitespace-only values are the same missing
|
|
4630
|
+
// argument (CONTRIBUTING.md CLI matrix); a flag-shaped `--phase --name x`
|
|
4631
|
+
// resolves to null in parseNamedArgs and lands here too. Runs before the
|
|
4632
|
+
// STATE.md existence check so argument validation always precedes I/O, and
|
|
4633
|
+
// before claimMilestonePhase so no phase-"null" milestone claim is taken.
|
|
4634
|
+
if (phaseNumber == null || String(phaseNumber).trim() === '') {
|
|
4635
|
+
error('phase required (--phase <N>)');
|
|
4636
|
+
}
|
|
4457
4637
|
const statePath = planningPaths(cwd).state;
|
|
4458
4638
|
if (!node_fs_1.default.existsSync(statePath)) {
|
|
4459
4639
|
output({ error: 'STATE.md not found' }, raw, undefined);
|
|
@@ -4467,7 +4647,11 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
4467
4647
|
// #1230 post-sync preservation, and the no-op write guard.
|
|
4468
4648
|
const intent = {
|
|
4469
4649
|
kind: 'beginPhase',
|
|
4470
|
-
|
|
4650
|
+
// The guard above made this non-null/non-empty; `error` is never-returning
|
|
4651
|
+
// at runtime but this module's destructured io binding does not narrow CFA,
|
|
4652
|
+
// so the narrowed fact is restated once (cmdStateUpdate's `field as string`
|
|
4653
|
+
// idiom, state.cts:782).
|
|
4654
|
+
phaseNumber: phaseNumber,
|
|
4471
4655
|
phaseName: phaseName ?? null,
|
|
4472
4656
|
planCount: planCount ?? null,
|
|
4473
4657
|
};
|
|
@@ -4763,6 +4947,12 @@ function updatePerformanceMetricsSection(content, cwd, phaseNum, planCount, summ
|
|
|
4763
4947
|
* Updates Status to "Ready to execute", Total Plans, Last Activity.
|
|
4764
4948
|
*/
|
|
4765
4949
|
function cmdStatePlannedPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
4950
|
+
// #4383: mirror begin-phase's command-boundary guard. A missing phase must
|
|
4951
|
+
// fail before even looking up STATE.md so no invalid invocation can enter
|
|
4952
|
+
// the read-modify-write path and serialize a null/blank phase identity.
|
|
4953
|
+
if (phaseNumber == null || String(phaseNumber).trim() === '') {
|
|
4954
|
+
error('phase required (--phase <N>)');
|
|
4955
|
+
}
|
|
4766
4956
|
const statePath = planningPaths(cwd).state;
|
|
4767
4957
|
if (!node_fs_1.default.existsSync(statePath)) {
|
|
4768
4958
|
output({ error: 'STATE.md not found' }, raw, undefined);
|
|
@@ -4778,7 +4968,7 @@ function cmdStatePlannedPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
4778
4968
|
// still owns the lock, the #1230 preservation, and the no-op write guard.
|
|
4779
4969
|
const intent = {
|
|
4780
4970
|
kind: 'plannedPhase',
|
|
4781
|
-
phaseNumber,
|
|
4971
|
+
phaseNumber: phaseNumber,
|
|
4782
4972
|
phaseName: phaseName ?? null,
|
|
4783
4973
|
planCount: planCount ?? null,
|
|
4784
4974
|
};
|
|
@@ -5134,7 +5324,10 @@ function cmdStateValidate(cwd, raw, opts = {}) {
|
|
|
5134
5324
|
// ("verification passed" drift), not a false S007.
|
|
5135
5325
|
const files = node_fs_1.default.readdirSync(phaseDirPath);
|
|
5136
5326
|
const phaseDirBaseName = node_path_1.default.basename(phaseDirPath);
|
|
5137
|
-
|
|
5327
|
+
// #612: `validateConvention` threaded (already resolved above for
|
|
5328
|
+
// `phaseKeyFromDir`) so the S006/S007 scan scopes bracket dirs by
|
|
5329
|
+
// their real token instead of the include-everything fail-safe.
|
|
5330
|
+
const verificationFiles = scopeToPhase(files.filter(f => f.includes('VERIFICATION') && f.endsWith('.md')), phaseDirBaseName, validateConvention);
|
|
5138
5331
|
for (const vf of verificationFiles) {
|
|
5139
5332
|
try {
|
|
5140
5333
|
const vContent = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDirPath, vf), 'utf-8');
|
|
@@ -5325,7 +5518,10 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
5325
5518
|
// was a second, independent consumer of the same raw field the initial
|
|
5326
5519
|
// migration missed — without it, `state sync` and `state json` disagreed
|
|
5327
5520
|
// on completed_phases for the identical disk state.
|
|
5328
|
-
|
|
5521
|
+
// #612: `syncConvention` threaded — the write-side half of
|
|
5522
|
+
// buildStateFrontmatter's thread above, so `state sync` and `state json`
|
|
5523
|
+
// keep agreeing on completed_phases under the bracket convention.
|
|
5524
|
+
if (isPhaseComplete(dirPath, { convention: syncConvention }).value.complete)
|
|
5329
5525
|
diskCompletedPhases++;
|
|
5330
5526
|
// Track the highest phase with incomplete plans (or any plans)
|
|
5331
5527
|
const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
|
|
@@ -364,14 +364,25 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
|
|
|
364
364
|
try {
|
|
365
365
|
for (const kind of layout.kinds) {
|
|
366
366
|
let staged;
|
|
367
|
-
|
|
367
|
+
// #4211: kimi-agents is an AGENT kind — kimiAgentsKind.stage() forwards
|
|
368
|
+
// agentCtx into stageAgentsForRuntimeWithConverter exactly as agentsKind
|
|
369
|
+
// does, and createRuntimeArtifactInstallPlan hands every kind the
|
|
370
|
+
// context. Staging it bare here dropped the path-prefix rewrites and the
|
|
371
|
+
// attribution trailer from Kimi's generated subagents, and (under an
|
|
372
|
+
// unmodified `full` profile) staged only the skill-referenced subset the
|
|
373
|
+
// install path stages with `skills: '*'`.
|
|
374
|
+
if (kind.kind === 'agents' || kind.kind === 'kimi-agents') {
|
|
368
375
|
const agentProfile = _isUnmodifiedFull ? { ...resolved, skills: '*' } : resolved;
|
|
369
376
|
staged = kind.stage(agentProfile, agentCtx);
|
|
370
377
|
}
|
|
371
378
|
else {
|
|
372
379
|
staged = kind.stage(resolved);
|
|
373
380
|
}
|
|
374
|
-
|
|
381
|
+
// #4211: kimi-agents takes the skill-body rewrite too —
|
|
382
|
+
// createRuntimeArtifactInstallPlan routes `skills` and `kimi-agents`
|
|
383
|
+
// through rewriteStagedSkillBodies together, so omitting it here left
|
|
384
|
+
// Kimi's surface-materialized prompts with unrewritten paths.
|
|
385
|
+
if (kind.kind === 'skills' || kind.kind === 'kimi-agents') {
|
|
375
386
|
runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
|
|
376
387
|
runtime: layout.runtime,
|
|
377
388
|
configDir: layout.configDir,
|
|
@@ -572,6 +583,53 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
|
|
|
572
583
|
// (no agentFileExtension declared) keep the staged filename verbatim.
|
|
573
584
|
const _agentExt = runtime ? runtimeArtifactConversion.agentFileExtensionFor(runtime) : undefined;
|
|
574
585
|
const isRenamedAgents = !!_agentExt && kindName === 'agents';
|
|
586
|
+
if (kindName === 'kimi-agents') {
|
|
587
|
+
// #4211: Kimi's managed tree is `gsd.yaml` + `gsd.md` + `subagents/gsd-*.{yaml,md}`
|
|
588
|
+
// (runtime-artifact-layout.cts kimiAgentsKind), and install copies it
|
|
589
|
+
// RECURSIVELY (_copyStaged in src/install-engine.cts). Surface apply fell
|
|
590
|
+
// through to the flat command/agent branch below, which reads only `*.md`
|
|
591
|
+
// at the top level: the YAML half and the whole subagents/ subtree were
|
|
592
|
+
// dropped, and `gsd.md` was written as `gsdgsd.md` (the flat branch
|
|
593
|
+
// re-applies kind.prefix to a name that already carries it). A surface
|
|
594
|
+
// change could therefore corrupt Kimi's installed artifacts while still
|
|
595
|
+
// reporting success.
|
|
596
|
+
node_fs_1.default.cpSync(stagedDir, destDir, { recursive: true });
|
|
597
|
+
// Prune GSD-owned files the new surface no longer stages, with exactly the
|
|
598
|
+
// ownership rule install's _removeGsdEntries applies to this kind: the two
|
|
599
|
+
// root files, and `gsd-`-prefixed .yaml/.md under subagents/. Everything
|
|
600
|
+
// else in the directory is user-owned and is preserved.
|
|
601
|
+
const _rootStaged = new Set(node_fs_1.default.readdirSync(stagedDir));
|
|
602
|
+
for (const fileName of ['gsd.yaml', 'gsd.md']) {
|
|
603
|
+
if (!_rootStaged.has(fileName)) {
|
|
604
|
+
try {
|
|
605
|
+
node_fs_1.default.rmSync(node_path_1.default.join(destDir, fileName), { force: true });
|
|
606
|
+
}
|
|
607
|
+
catch { /* ignore */ }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const _stagedSubagentsDir = node_path_1.default.join(stagedDir, 'subagents');
|
|
611
|
+
const _destSubagentsDir = node_path_1.default.join(destDir, 'subagents');
|
|
612
|
+
const _stagedSubagents = node_fs_1.default.existsSync(_stagedSubagentsDir)
|
|
613
|
+
? new Set(node_fs_1.default.readdirSync(_stagedSubagentsDir))
|
|
614
|
+
: new Set();
|
|
615
|
+
if (node_fs_1.default.existsSync(_destSubagentsDir)) {
|
|
616
|
+
for (const entry of node_fs_1.default.readdirSync(_destSubagentsDir, { withFileTypes: true })) {
|
|
617
|
+
if (!entry.isFile())
|
|
618
|
+
continue;
|
|
619
|
+
if (!entry.name.startsWith('gsd-'))
|
|
620
|
+
continue;
|
|
621
|
+
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.md'))
|
|
622
|
+
continue;
|
|
623
|
+
if (_stagedSubagents.has(entry.name))
|
|
624
|
+
continue;
|
|
625
|
+
try {
|
|
626
|
+
node_fs_1.default.rmSync(node_path_1.default.join(_destSubagentsDir, entry.name), { force: true });
|
|
627
|
+
}
|
|
628
|
+
catch { /* ignore */ }
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
575
633
|
if (kindName === 'skills') {
|
|
576
634
|
// Skills kind: work with directories, not files.
|
|
577
635
|
// Each staged entry is a directory named ${prefix}${stem}.
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const security_cjs_1 = require("./security.cjs");
|
|
14
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
16
|
const ioMod = require("./io.cjs");
|
|
16
17
|
const { output, error, ERROR_REASON } = ioMod;
|
|
@@ -108,12 +109,14 @@ function routeResolveContent({ args, cwd, raw }, deps = {}) {
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
const projectRoot = node_path_1.default.resolve(cwd || process.cwd());
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Lexical containment (ADR-4650): this path is validated before existence is
|
|
113
|
+
// checked below, so realpath resolution is neither available nor required.
|
|
114
|
+
const contained = (0, security_cjs_1.tryWithinRootLexical)(plan, projectRoot);
|
|
115
|
+
if (contained === null) {
|
|
114
116
|
error(`Plan file is outside project scope: ${plan}`, ERROR_REASON.USAGE);
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
119
|
+
const resolvedPlanPath = contained;
|
|
117
120
|
if (!node_fs_1.default.existsSync(resolvedPlanPath)) {
|
|
118
121
|
error(`Plan file not found: ${plan}`, ERROR_REASON.USAGE);
|
|
119
122
|
return;
|
|
@@ -173,11 +176,14 @@ function routeTaskCommand({ args, cwd, raw }) {
|
|
|
173
176
|
else if (args[2]) {
|
|
174
177
|
const projectRoot = node_path_1.default.resolve(cwd || process.cwd());
|
|
175
178
|
const requestedPath = args[2];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
// Lexical containment (ADR-4650): validated before existence is checked below.
|
|
180
|
+
// `error()` here does not return/throw (preserved from before this migration),
|
|
181
|
+
// so resolvedTaskPath must still be computed identically on the rejected path.
|
|
182
|
+
const contained = (0, security_cjs_1.tryWithinRootLexical)(requestedPath, projectRoot);
|
|
183
|
+
if (contained === null) {
|
|
179
184
|
error(`Task file is outside project scope: ${requestedPath}`, ERROR_REASON.USAGE);
|
|
180
185
|
}
|
|
186
|
+
const resolvedTaskPath = contained ?? node_path_1.default.resolve(projectRoot, requestedPath);
|
|
181
187
|
if (!node_fs_1.default.existsSync(resolvedTaskPath)) {
|
|
182
188
|
error(`Task file not found: ${requestedPath}`, ERROR_REASON.USAGE);
|
|
183
189
|
}
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -322,7 +322,7 @@ function cmdRenderCheckpoint(cwd, options = {}, raw) {
|
|
|
322
322
|
if (!filePath) {
|
|
323
323
|
error('UAT file required: use uat render-checkpoint --file <path>');
|
|
324
324
|
}
|
|
325
|
-
const resolvedPath = (0, security_cjs_1.requireSafePath)(filePath, cwd, 'UAT file',
|
|
325
|
+
const resolvedPath = (0, security_cjs_1.requireSafePath)(filePath, cwd, 'UAT file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
326
326
|
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
327
327
|
error(`UAT file not found: ${filePath}`);
|
|
328
328
|
}
|
|
@@ -127,6 +127,23 @@ function preferFirst(entries, preferred) {
|
|
|
127
127
|
const rest = entries.filter(([rt]) => rt !== preferred);
|
|
128
128
|
return [...pref, ...rest];
|
|
129
129
|
}
|
|
130
|
+
// GLOBAL probe: absolute env candidates first (in preferFirst order, first
|
|
131
|
+
// hasInstall hit wins), then $HOME-relative. Single resolver shared by the
|
|
132
|
+
// preferredConfigDir fast path's same-path dedup and the full cascade (#4197),
|
|
133
|
+
// so both compare against the global dir the resolution would actually select —
|
|
134
|
+
// an env-directed candidate, not necessarily the $HOME-relative pathname.
|
|
135
|
+
function resolveGlobalCandidate(fs, env, home, preferred) {
|
|
136
|
+
for (const [rt, absdir] of preferFirst(envRuntimeDirs({ env, home }), preferred)) {
|
|
137
|
+
if (hasInstall(fs, absdir))
|
|
138
|
+
return { runtime: rt, dir: node_path_1.default.resolve(absdir) };
|
|
139
|
+
}
|
|
140
|
+
for (const [rt, reldir] of preferFirst(exports.RUNTIME_DIRS, preferred)) {
|
|
141
|
+
const cand = node_path_1.default.resolve(home, reldir);
|
|
142
|
+
if (hasInstall(fs, cand))
|
|
143
|
+
return { runtime: rt, dir: cand };
|
|
144
|
+
}
|
|
145
|
+
return { runtime: '', dir: '' };
|
|
146
|
+
}
|
|
130
147
|
/**
|
|
131
148
|
* Pure resolver. Returns { installedVersion, scope, runtime, gsdDir }.
|
|
132
149
|
*/
|
|
@@ -137,11 +154,17 @@ function resolveUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = ''
|
|
|
137
154
|
// Fast path: a validated preferredConfigDir (custom --config-dir install).
|
|
138
155
|
if (preferredConfigDir && hasInstall(fs, preferredConfigDir)) {
|
|
139
156
|
const resolvedPref = node_path_1.default.resolve(preferredConfigDir);
|
|
157
|
+
// Same-path dedup the cascade applies (#4197): a preferred dir that IS the
|
|
158
|
+
// selected global install (an env candidate or the $HOME-relative dir) is
|
|
159
|
+
// GLOBAL even when cwd === $HOME also makes it the cwd-relative match.
|
|
160
|
+
const { dir: globalDir } = resolveGlobalCandidate(fs, env, home, preferred);
|
|
140
161
|
let scope = 'GLOBAL';
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
if (resolvedPref !== globalDir) {
|
|
163
|
+
for (const [, reldir] of exports.RUNTIME_DIRS) {
|
|
164
|
+
if (node_path_1.default.resolve(cwd, reldir) === resolvedPref) {
|
|
165
|
+
scope = 'LOCAL';
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
return {
|
|
@@ -151,7 +174,6 @@ function resolveUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = ''
|
|
|
151
174
|
gsdDir: preferredConfigDir,
|
|
152
175
|
};
|
|
153
176
|
}
|
|
154
|
-
const orderedEnv = preferFirst(envRuntimeDirs({ env, home }), preferred);
|
|
155
177
|
const orderedRuntime = preferFirst(exports.RUNTIME_DIRS, preferred);
|
|
156
178
|
// LOCAL probe (relative to cwd).
|
|
157
179
|
let localRuntime = '', localDir = '';
|
|
@@ -163,25 +185,9 @@ function resolveUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = ''
|
|
|
163
185
|
break;
|
|
164
186
|
}
|
|
165
187
|
}
|
|
166
|
-
// GLOBAL probe: absolute env candidates first, then $HOME-relative
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (hasInstall(fs, absdir)) {
|
|
170
|
-
globalRuntime = rt;
|
|
171
|
-
globalDir = node_path_1.default.resolve(absdir);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (!globalRuntime) {
|
|
176
|
-
for (const [rt, reldir] of orderedRuntime) {
|
|
177
|
-
const cand = node_path_1.default.resolve(home, reldir);
|
|
178
|
-
if (hasInstall(fs, cand)) {
|
|
179
|
-
globalRuntime = rt;
|
|
180
|
-
globalDir = cand;
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
188
|
+
// GLOBAL probe: absolute env candidates first, then $HOME-relative — the
|
|
189
|
+
// same resolver the fast path dedups against.
|
|
190
|
+
const { runtime: globalRuntime, dir: globalDir } = resolveGlobalCandidate(fs, env, home, preferred);
|
|
185
191
|
const localValid = trustedVersionAt(fs, localDir);
|
|
186
192
|
const isLocal = !!localValid && (!globalDir || localDir !== globalDir);
|
|
187
193
|
if (isLocal) {
|
|
@@ -1262,16 +1262,21 @@
|
|
|
1262
1262
|
state.result += _result;
|
|
1263
1263
|
}
|
|
1264
1264
|
}
|
|
1265
|
+
function chargeMergeWork(state) {
|
|
1266
|
+
state.totalMergeKeys++;
|
|
1267
|
+
if (state.maxTotalMergeKeys !== -1 && state.totalMergeKeys > state.maxTotalMergeKeys) {
|
|
1268
|
+
throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1265
1271
|
function mergeMappings(state, destination, source, overridableKeys) {
|
|
1266
1272
|
if (!common2.isObject(source)) {
|
|
1267
1273
|
throwError(state, "cannot merge mappings; the provided source object is unacceptable");
|
|
1268
1274
|
}
|
|
1275
|
+
chargeMergeWork(state);
|
|
1269
1276
|
var sourceKeys = Object.keys(source);
|
|
1270
1277
|
for (var index = 0, quantity = sourceKeys.length; index < quantity; index += 1) {
|
|
1271
1278
|
var key = sourceKeys[index];
|
|
1272
|
-
|
|
1273
|
-
throwError(state, "merge keys exceeded maxTotalMergeKeys (" + state.maxTotalMergeKeys + ")");
|
|
1274
|
-
}
|
|
1279
|
+
chargeMergeWork(state);
|
|
1275
1280
|
if (!_hasOwnProperty.call(destination, key)) {
|
|
1276
1281
|
setProperty(destination, key, source[key]);
|
|
1277
1282
|
overridableKeys[key] = true;
|
|
@@ -1299,6 +1304,9 @@
|
|
|
1299
1304
|
}
|
|
1300
1305
|
if (keyTag === "tag:yaml.org,2002:merge") {
|
|
1301
1306
|
if (Array.isArray(valueNode)) {
|
|
1307
|
+
if (valueNode.length > 100) {
|
|
1308
|
+
throwError(state, "abnormal merge sequence size");
|
|
1309
|
+
}
|
|
1302
1310
|
for (var _index = 0, _quantity = valueNode.length; _index < _quantity; _index += 1) {
|
|
1303
1311
|
mergeMappings(state, _result, valueNode[_index], overridableKeys);
|
|
1304
1312
|
}
|