sillyspec 3.27.2 → 3.27.6
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/skills/sillyspec-execute/SKILL.md +8 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -1
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.claude/worktrees/sillyspec-data-model-research/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.claude/worktrees/sillyspec-data-model-research/.gitattributes +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/.husky/pre-push +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/README.md +203 -0
- package/.claude/worktrees/sillyspec-data-model-research/SKILL.md +91 -0
- package/.claude/worktrees/sillyspec-data-model-research/bin/sillyspec.js +2 -0
- package/.claude/worktrees/sillyspec-data-model-research/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/package-lock.json +977 -0
- package/.claude/worktrees/sillyspec-data-model-research/package.json +39 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/favicon.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/index.html +16 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/logo.jpg +0 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/package.json +25 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/executor.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/index.js +679 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/parser.js +526 -0
- package/.claude/worktrees/sillyspec-data-model-research/packages/dashboard/server/watcher.js +357 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/agent-session-log.js +803 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-list.js +290 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/change-risk-profile.js +270 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/check-primitives.js +65 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/classify-change.js +109 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/commit-suggest.js +202 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-cat.js +88 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/config-schema.js +328 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/constants.js +97 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/contract-matrix.js +480 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/datetime.js +20 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db-engine.js +55 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/db.js +381 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/decision-distill.js +517 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/local-agent.js +155 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/probe.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/dispatch/strategy.js +121 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-check.js +825 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-debt.js +393 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/docs-gate.js +107 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/doctor-diagnostics.js +1099 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/endpoint-extractor.js +420 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/foreign-declared.js +78 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/fs-atomic.js +73 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/git-helper.js +150 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/hooks/worktree-guard.js +892 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/index.js +2798 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/init.js +626 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/knowledge-match.js +231 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-detect.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/local-register.js +104 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/machine-interface.js +543 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/migrate.js +127 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-impact.js +166 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/module-resolve.js +222 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/modules.js +580 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/change-registry.js +336 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/consistency-doctor.js +355 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/shared.js +36 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/stage-machine.js +594 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress/step-store.js +265 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/progress.js +1304 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quick-recommend.js +115 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/quicklog.js +852 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/review-tier.js +77 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/command.js +1595 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete-handlers.js +1552 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/complete.js +1259 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/concurrent-detect.js +156 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/gates.js +1084 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/multi-repo-context.js +208 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/next.js +128 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/prompt.js +843 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/quick-audit.js +186 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/scan-profile.js +269 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/shared.js +1562 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run/stage.js +584 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/run.js +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-diff.js +388 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-postcheck.js +512 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/scan-staleness.js +95 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/setup.js +414 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/client.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sillyhub-mcp/config.js +62 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-dir-typo.js +50 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/spec-sync.js +358 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-engine.js +171 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract-spec.js +550 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-contract.js +889 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stage-review.js +597 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/archive.js +199 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm-auto.js +236 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/brainstorm.js +617 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/cmd-existence.js +145 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/doctor.js +540 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/execute.js +1275 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/explore.js +43 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/index.js +29 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/knowledge.js +514 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan-postcheck.js +1344 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/plan.js +650 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/quick.js +167 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/scan.js +608 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/status.js +64 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/stages/verify.js +246 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/sync.js +1470 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/task-review.js +1498 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/taskcard.js +210 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-postcheck.js +1718 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/verify-probes.js +277 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/version.js +19 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workflow.js +600 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/workspace.js +125 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-apply.js +1446 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree-deps.js +576 -0
- package/.claude/worktrees/sillyspec-data-model-research/src/worktree.js +1715 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/claude-instruction.md +23 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/taskcard-rules.md +28 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/testcase-design.md +12 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/prompts/verify-probes.md +32 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/archive-impact.yaml +79 -0
- package/.claude/worktrees/sillyspec-data-model-research/templates/workflows/scan-docs.yaml +132 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-auto/SKILL.md +93 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-continue/SKILL.md +47 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-execute/SKILL.md +214 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-explore/SKILL.md +102 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-propose/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-resume/SKILL.md +70 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-state/SKILL.md +64 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/.worktrees/8d2d0392/.claude/skills/sillyspec-workspace/SKILL.md +175 -0
- package/.worktrees/8d2d0392/.claude/skills/verify-per-user/SKILL.md +6 -0
- package/.worktrees/8d2d0392/.gitattributes +2 -0
- package/.worktrees/8d2d0392/.husky/pre-push +20 -0
- package/.worktrees/8d2d0392/README.md +203 -0
- package/.worktrees/8d2d0392/SKILL.md +91 -0
- package/.worktrees/8d2d0392/architecture-analysis.md +448 -0
- package/.worktrees/8d2d0392/bin/sillyspec.js +2 -0
- package/.worktrees/8d2d0392/eval/README.md +86 -0
- package/.worktrees/8d2d0392/eval/eval.config.json +28 -0
- package/.worktrees/8d2d0392/eval/lib/adapters/cli.mjs +63 -0
- package/.worktrees/8d2d0392/eval/lib/adapters/dryrun.mjs +27 -0
- package/.worktrees/8d2d0392/eval/lib/config.mjs +82 -0
- package/.worktrees/8d2d0392/eval/lib/db.mjs +76 -0
- package/.worktrees/8d2d0392/eval/lib/report.mjs +120 -0
- package/.worktrees/8d2d0392/eval/lib/runner.mjs +157 -0
- package/.worktrees/8d2d0392/eval/lib/tasks.mjs +73 -0
- package/.worktrees/8d2d0392/eval/lib/util.mjs +107 -0
- package/.worktrees/8d2d0392/eval/lib/workdir.mjs +30 -0
- package/.worktrees/8d2d0392/eval/run.mjs +83 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/001-slugify/verify.mjs +28 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/002-each-day/verify.mjs +31 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/files/package.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/task.json +6 -0
- package/.worktrees/8d2d0392/eval/tasks/003-word-report/verify.mjs +38 -0
- package/.worktrees/8d2d0392/logo.jpg +0 -0
- package/.worktrees/8d2d0392/package-lock.json +977 -0
- package/.worktrees/8d2d0392/package.json +41 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/favicon.jpg +0 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/index.html +16 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/logo.jpg +0 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/.worktrees/8d2d0392/packages/dashboard/dist/prototype-overview.html +256 -0
- package/.worktrees/8d2d0392/packages/dashboard/package.json +25 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/executor.js +95 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/index.js +679 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/parser.js +526 -0
- package/.worktrees/8d2d0392/packages/dashboard/server/watcher.js +357 -0
- package/.worktrees/8d2d0392/src/agent-session-log.js +803 -0
- package/.worktrees/8d2d0392/src/change-list.js +290 -0
- package/.worktrees/8d2d0392/src/change-risk-profile.js +339 -0
- package/.worktrees/8d2d0392/src/check-primitives.js +65 -0
- package/.worktrees/8d2d0392/src/classify-change.js +109 -0
- package/.worktrees/8d2d0392/src/commit-suggest.js +202 -0
- package/.worktrees/8d2d0392/src/config-cat.js +88 -0
- package/.worktrees/8d2d0392/src/config-schema.js +328 -0
- package/.worktrees/8d2d0392/src/constants.js +97 -0
- package/.worktrees/8d2d0392/src/contract-matrix.js +533 -0
- package/.worktrees/8d2d0392/src/datetime.js +20 -0
- package/.worktrees/8d2d0392/src/db-engine.js +55 -0
- package/.worktrees/8d2d0392/src/db.js +381 -0
- package/.worktrees/8d2d0392/src/decision-distill.js +517 -0
- package/.worktrees/8d2d0392/src/dispatch/backends/local-agent.js +155 -0
- package/.worktrees/8d2d0392/src/dispatch/backends/sillyhub-mcp.js +245 -0
- package/.worktrees/8d2d0392/src/dispatch/probe.js +246 -0
- package/.worktrees/8d2d0392/src/dispatch/strategy.js +121 -0
- package/.worktrees/8d2d0392/src/docs-check.js +825 -0
- package/.worktrees/8d2d0392/src/docs-debt.js +393 -0
- package/.worktrees/8d2d0392/src/docs-gate.js +107 -0
- package/.worktrees/8d2d0392/src/doctor-diagnostics.js +1099 -0
- package/.worktrees/8d2d0392/src/endpoint-extractor.js +409 -0
- package/.worktrees/8d2d0392/src/foreign-declared.js +78 -0
- package/.worktrees/8d2d0392/src/fs-atomic.js +73 -0
- package/.worktrees/8d2d0392/src/git-helper.js +150 -0
- package/.worktrees/8d2d0392/src/hooks/claude-pre-tool-use.cjs +130 -0
- package/.worktrees/8d2d0392/src/hooks/worktree-guard.js +892 -0
- package/.worktrees/8d2d0392/src/index.js +2854 -0
- package/.worktrees/8d2d0392/src/init.js +626 -0
- package/.worktrees/8d2d0392/src/knowledge-match.js +231 -0
- package/.worktrees/8d2d0392/src/local-detect.js +128 -0
- package/.worktrees/8d2d0392/src/local-register.js +104 -0
- package/.worktrees/8d2d0392/src/machine-interface.js +543 -0
- package/.worktrees/8d2d0392/src/migrate.js +127 -0
- package/.worktrees/8d2d0392/src/module-impact.js +166 -0
- package/.worktrees/8d2d0392/src/module-resolve.js +222 -0
- package/.worktrees/8d2d0392/src/modules.js +580 -0
- package/.worktrees/8d2d0392/src/plan-adopt-waves.js +126 -0
- package/.worktrees/8d2d0392/src/progress/change-registry.js +336 -0
- package/.worktrees/8d2d0392/src/progress/consistency-doctor.js +355 -0
- package/.worktrees/8d2d0392/src/progress/shared.js +36 -0
- package/.worktrees/8d2d0392/src/progress/stage-machine.js +594 -0
- package/.worktrees/8d2d0392/src/progress/step-store.js +265 -0
- package/.worktrees/8d2d0392/src/progress.js +1304 -0
- package/.worktrees/8d2d0392/src/quick-recommend.js +115 -0
- package/.worktrees/8d2d0392/src/quicklog.js +883 -0
- package/.worktrees/8d2d0392/src/review-tier.js +77 -0
- package/.worktrees/8d2d0392/src/run/command.js +1641 -0
- package/.worktrees/8d2d0392/src/run/complete-handlers.js +1552 -0
- package/.worktrees/8d2d0392/src/run/complete.js +1259 -0
- package/.worktrees/8d2d0392/src/run/concurrent-detect.js +156 -0
- package/.worktrees/8d2d0392/src/run/gates.js +1094 -0
- package/.worktrees/8d2d0392/src/run/multi-repo-context.js +208 -0
- package/.worktrees/8d2d0392/src/run/next.js +128 -0
- package/.worktrees/8d2d0392/src/run/prompt.js +843 -0
- package/.worktrees/8d2d0392/src/run/quick-audit.js +186 -0
- package/.worktrees/8d2d0392/src/run/scan-profile.js +269 -0
- package/.worktrees/8d2d0392/src/run/shared.js +1562 -0
- package/.worktrees/8d2d0392/src/run/stage.js +587 -0
- package/.worktrees/8d2d0392/src/run.js +23 -0
- package/.worktrees/8d2d0392/src/scan-diff.js +388 -0
- package/.worktrees/8d2d0392/src/scan-postcheck.js +524 -0
- package/.worktrees/8d2d0392/src/scan-staleness.js +95 -0
- package/.worktrees/8d2d0392/src/setup.js +414 -0
- package/.worktrees/8d2d0392/src/sillyhub-mcp/client.js +597 -0
- package/.worktrees/8d2d0392/src/sillyhub-mcp/config.js +62 -0
- package/.worktrees/8d2d0392/src/spec-dir-typo.js +50 -0
- package/.worktrees/8d2d0392/src/spec-sync.js +358 -0
- package/.worktrees/8d2d0392/src/stage-contract-engine.js +171 -0
- package/.worktrees/8d2d0392/src/stage-contract-spec.js +551 -0
- package/.worktrees/8d2d0392/src/stage-contract.js +948 -0
- package/.worktrees/8d2d0392/src/stage-review.js +597 -0
- package/.worktrees/8d2d0392/src/stages/archive.js +199 -0
- package/.worktrees/8d2d0392/src/stages/brainstorm-auto.js +285 -0
- package/.worktrees/8d2d0392/src/stages/brainstorm.js +624 -0
- package/.worktrees/8d2d0392/src/stages/cmd-existence.js +145 -0
- package/.worktrees/8d2d0392/src/stages/doctor.js +540 -0
- package/.worktrees/8d2d0392/src/stages/execute.js +1277 -0
- package/.worktrees/8d2d0392/src/stages/explore.js +43 -0
- package/.worktrees/8d2d0392/src/stages/index.js +29 -0
- package/.worktrees/8d2d0392/src/stages/knowledge.js +514 -0
- package/.worktrees/8d2d0392/src/stages/plan-postcheck.js +1398 -0
- package/.worktrees/8d2d0392/src/stages/plan.js +651 -0
- package/.worktrees/8d2d0392/src/stages/quick.js +167 -0
- package/.worktrees/8d2d0392/src/stages/scan.js +608 -0
- package/.worktrees/8d2d0392/src/stages/status.js +64 -0
- package/.worktrees/8d2d0392/src/stages/verify.js +246 -0
- package/.worktrees/8d2d0392/src/sync.js +1470 -0
- package/.worktrees/8d2d0392/src/task-review.js +1498 -0
- package/.worktrees/8d2d0392/src/taskcard-placeholders.js +21 -0
- package/.worktrees/8d2d0392/src/taskcard.js +217 -0
- package/.worktrees/8d2d0392/src/verify-postcheck.js +1718 -0
- package/.worktrees/8d2d0392/src/verify-probes.js +327 -0
- package/.worktrees/8d2d0392/src/version.js +19 -0
- package/.worktrees/8d2d0392/src/workflow.js +600 -0
- package/.worktrees/8d2d0392/src/workspace.js +125 -0
- package/.worktrees/8d2d0392/src/worktree-apply.js +1611 -0
- package/.worktrees/8d2d0392/src/worktree-deps.js +576 -0
- package/.worktrees/8d2d0392/src/worktree.js +1838 -0
- package/.worktrees/8d2d0392/templates/claude-instruction.md +23 -0
- package/.worktrees/8d2d0392/templates/prompts/taskcard-rules.md +29 -0
- package/.worktrees/8d2d0392/templates/prompts/testcase-design.md +12 -0
- package/.worktrees/8d2d0392/templates/prompts/verify-probes.md +32 -0
- package/.worktrees/8d2d0392/templates/workflows/archive-impact.yaml +79 -0
- package/.worktrees/8d2d0392/templates/workflows/scan-docs.yaml +132 -0
- package/architecture-analysis.md +448 -0
- package/eval/README.md +86 -0
- package/eval/eval.config.json +28 -0
- package/eval/lib/adapters/cli.mjs +63 -0
- package/eval/lib/adapters/dryrun.mjs +27 -0
- package/eval/lib/config.mjs +82 -0
- package/eval/lib/db.mjs +76 -0
- package/eval/lib/report.mjs +120 -0
- package/eval/lib/runner.mjs +157 -0
- package/eval/lib/tasks.mjs +73 -0
- package/eval/lib/util.mjs +107 -0
- package/eval/lib/workdir.mjs +30 -0
- package/eval/run.mjs +83 -0
- package/eval/tasks/001-slugify/files/package.json +6 -0
- package/eval/tasks/001-slugify/solution/src/slugify.mjs +26 -0
- package/eval/tasks/001-slugify/task.json +6 -0
- package/eval/tasks/001-slugify/verify.mjs +28 -0
- package/eval/tasks/002-each-day/files/package.json +6 -0
- package/eval/tasks/002-each-day/files/src/dates.mjs +4 -0
- package/eval/tasks/002-each-day/solution/src/dates.mjs +30 -0
- package/eval/tasks/002-each-day/task.json +6 -0
- package/eval/tasks/002-each-day/verify.mjs +31 -0
- package/eval/tasks/003-word-report/files/data/notes.txt +5 -0
- package/eval/tasks/003-word-report/files/package.json +6 -0
- package/eval/tasks/003-word-report/solution/bin/report.mjs +36 -0
- package/eval/tasks/003-word-report/task.json +6 -0
- package/eval/tasks/003-word-report/verify.mjs +38 -0
- package/package.json +4 -2
- package/src/agent-session-log.js +45 -5
- package/src/change-risk-profile.js +83 -14
- package/src/config-schema.js +18 -3
- package/src/contract-matrix.js +90 -29
- package/src/datetime.js +20 -0
- package/src/decision-distill.js +517 -0
- package/src/dispatch/backends/local-agent.js +4 -4
- package/src/docs-check.js +227 -0
- package/src/docs-debt.js +187 -24
- package/src/doctor-diagnostics.js +102 -2
- package/src/endpoint-extractor.js +41 -52
- package/src/foreign-declared.js +155 -0
- package/src/index.js +95 -13
- package/src/init.js +9 -7
- package/src/knowledge-match.js +107 -6
- package/src/local-register.js +7 -1
- package/src/module-resolve.js +12 -1
- package/src/modules.js +2 -1
- package/src/plan-adopt-waves.js +126 -0
- package/src/quicklog.js +50 -4
- package/src/run/command.js +124 -44
- package/src/run/complete-handlers.js +48 -19
- package/src/run/complete.js +5 -2
- package/src/run/gates.js +104 -22
- package/src/run/prompt.js +84 -42
- package/src/run/quick-audit.js +11 -0
- package/src/run/shared.js +152 -10
- package/src/run/stage.js +8 -5
- package/src/run.js +2 -2
- package/src/scan-postcheck.js +30 -17
- package/src/stage-contract-spec.js +3 -2
- package/src/stage-contract.js +68 -4
- package/src/stages/archive.js +53 -12
- package/src/stages/brainstorm-auto.js +50 -1
- package/src/stages/brainstorm.js +13 -0
- package/src/stages/doctor.js +157 -1
- package/src/stages/execute.js +32 -6
- package/src/stages/plan-postcheck.js +76 -17
- package/src/stages/plan.js +9 -4
- package/src/stages/quick.js +6 -1
- package/src/stages/verify.js +32 -15
- package/src/sync.js +41 -3
- package/src/task-review.js +2 -2
- package/src/taskcard-placeholders.js +21 -0
- package/src/taskcard.js +43 -1
- package/src/verify-postcheck.js +315 -13
- package/src/verify-probes.js +73 -10
- package/src/worktree-apply.js +569 -227
- package/src/worktree-deps.js +150 -1
- package/src/worktree.js +285 -77
- package/templates/prompts/taskcard-rules.md +2 -1
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quicklog.js — QUICKLOG 记录的 CLI 接管层
|
|
3
|
+
*
|
|
4
|
+
* 历史问题:QUICKLOG 条目(ql-ID 分配 + 追加)原本由 Agent(LLM)手写
|
|
5
|
+
* (见 src/stages/quick.js 旧 prompt)。两个后果:
|
|
6
|
+
* 1. 漏写静默通过:CLI 只查目录级存在性,agent 漏写仍报「3/3 SAFE」。
|
|
7
|
+
* 2. 并发丢更新:多 quick 会话并发写同一 per-user QUICKLOG 文件,读-改-写非原子,
|
|
8
|
+
* 已有实证(QUICKLOG-qinyi.md 同一 ql-20260604-001-7a4c 出现两次)。
|
|
9
|
+
*
|
|
10
|
+
* 本模块把分配与写入下沉到 CLI 进程内,O_EXCL lockfile 串行化,彻底消除上述问题。
|
|
11
|
+
* 无新 npm 依赖(仅 fs/path/crypto)——匹配项目零 FS 工具依赖的风格。
|
|
12
|
+
*/
|
|
13
|
+
import { join, dirname, basename } from 'path'
|
|
14
|
+
import { getRule } from './stage-contract-spec.js'
|
|
15
|
+
import {
|
|
16
|
+
openSync, closeSync, unlinkSync, statSync, mkdirSync, existsSync,
|
|
17
|
+
readFileSync, writeFileSync, writeSync, readdirSync, renameSync, appendFileSync, rmSync,
|
|
18
|
+
} from 'fs'
|
|
19
|
+
import { randomBytes } from 'crypto'
|
|
20
|
+
|
|
21
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* O_EXCL 文件锁。`openSync(lockPath,'wx')` 独占创建并写入持有者唯一 id;已存在则按
|
|
25
|
+
* mtime 判 stale 偷锁(rename 原子抢占 + 内容校验),否则退避重试至超时。
|
|
26
|
+
* CLI 短进程、低频写入,stale 阈值 30s 足够。
|
|
27
|
+
*
|
|
28
|
+
* 偷锁为何用 rename 而非 unlink(2026-08-20 体检 BUG-02):两进程同偷一把 stale 锁时,
|
|
29
|
+
* 后到者的 unlink 会把先到者刚重建的新锁删掉(TOCTOU)→ 双进程同时进临界区。
|
|
30
|
+
* rename 到唯一名后比对内容:只有"观察时看到的那把锁"才允许删除;偷到并发者
|
|
31
|
+
* 新建的锁则原路放回。残余窗口仅剩"纳秒级三方精确竞争"(放回前 existsSync 检查
|
|
32
|
+
* 与 rename 之间),相比旧实现的必现双进已可忽略。
|
|
33
|
+
* @param {string} lockPath 锁文件路径(建议与被保护文件同目录)
|
|
34
|
+
* @param {() => (any|Promise<any>)} fn 临界区
|
|
35
|
+
* @param {{staleMs?:number, timeoutMs?:number, retryMs?:number, content?:string}} opts
|
|
36
|
+
* content:锁文件内容(默认 pid-randomId)。供超时报错时展示持有者语义(如 apply 锁写
|
|
37
|
+
* {pid, changeName})——仅展示用途,抢占/偷锁逻辑只比对内容一致性不解析语义。
|
|
38
|
+
*/
|
|
39
|
+
export async function withFileLock(lockPath, fn, opts = {}) {
|
|
40
|
+
const { staleMs = 30000, timeoutMs = 10000, retryMs = 50, content = null } = opts
|
|
41
|
+
mkdirSync(dirname(lockPath), { recursive: true })
|
|
42
|
+
const start = Date.now()
|
|
43
|
+
const myId = content || `${process.pid}-${randomBytes(6).toString('hex')}`
|
|
44
|
+
let fd = null
|
|
45
|
+
// 抢锁循环
|
|
46
|
+
while (true) {
|
|
47
|
+
try {
|
|
48
|
+
fd = openSync(lockPath, 'wx') // O_EXCL:已存在抛 EEXIST
|
|
49
|
+
writeSync(fd, myId)
|
|
50
|
+
break
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e.code !== 'EEXIST') throw e
|
|
53
|
+
// 锁已存在:判 stale(持有进程崩溃残留)
|
|
54
|
+
try {
|
|
55
|
+
const mtime = statSync(lockPath).mtimeMs
|
|
56
|
+
if (Date.now() - mtime > staleMs) {
|
|
57
|
+
try {
|
|
58
|
+
const observed = readFileSync(lockPath, 'utf8') // 观察到的持有者 id
|
|
59
|
+
const claim = `${lockPath}.steal-${randomBytes(6).toString('hex')}`
|
|
60
|
+
renameSync(lockPath, claim) // 原子抢占:并发者只可能有一个 rename 成功
|
|
61
|
+
if (readFileSync(claim, 'utf8') === observed) {
|
|
62
|
+
try { unlinkSync(claim) } catch {} // 确实是那把 stale 锁 → 回收
|
|
63
|
+
} else {
|
|
64
|
+
// 偷到并发者新建的锁:放回(原位已被占则放弃,先占者保留)
|
|
65
|
+
let restored = false
|
|
66
|
+
try { if (!existsSync(lockPath)) { renameSync(claim, lockPath); restored = true } } catch {}
|
|
67
|
+
if (!restored) { try { unlinkSync(claim) } catch {} }
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
if (Date.now() - start > timeoutMs) {
|
|
74
|
+
throw new Error(`文件锁超时(${timeoutMs}ms): ${lockPath}`)
|
|
75
|
+
}
|
|
76
|
+
await sleep(retryMs)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return await fn()
|
|
81
|
+
} finally {
|
|
82
|
+
try { closeSync(fd) } catch {}
|
|
83
|
+
try { unlinkSync(lockPath) } catch {} // 释放(容忍已不存在)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── 内部工具 ──
|
|
88
|
+
|
|
89
|
+
function todayStamp(d = new Date()) {
|
|
90
|
+
return d.getFullYear()
|
|
91
|
+
+ String(d.getMonth() + 1).padStart(2, '0')
|
|
92
|
+
+ String(d.getDate()).padStart(2, '0')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nowDatetime(d = new Date()) {
|
|
96
|
+
return todayStamp(d).slice(0, 4) + '-' + todayStamp(d).slice(4, 6) + '-' + todayStamp(d).slice(6, 8)
|
|
97
|
+
+ ' ' + String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 描述压成一行、限长,避免破坏 QUICKLOG 条目结构
|
|
101
|
+
export function sanitizeDesc(description) {
|
|
102
|
+
const s = String(description || '').replace(/[\r\n]+/g, ' ').trim()
|
|
103
|
+
if (!s) return '(quick 任务)'
|
|
104
|
+
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
// ── 平台推送(best-effort,2026-08-16-change-center-quick-tab task-06 / D-003)──
|
|
109
|
+
|
|
110
|
+
// 读 local.yaml platform 段(url+token)。quicklog 不 import sync.js(会拖 ProgressManager
|
|
111
|
+
// 整链),自带同款轻量解析:只取 platform.url / platform.token 两键,与 parseSimpleYaml
|
|
112
|
+
// 同风格(丢注释可接受——只读)。未连接返回 null(合法本地状态,静默跳过)。
|
|
113
|
+
function _readPlatformConfig(specBase) {
|
|
114
|
+
try {
|
|
115
|
+
// specBase 即 <cwd>/.sillyspec(或平台 specRoot);local.yaml 就在 spec 根下
|
|
116
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
117
|
+
if (!existsSync(yamlPath)) return null
|
|
118
|
+
const lines = readFileSync(yamlPath, 'utf8').split(/\r?\n/)
|
|
119
|
+
let inPlatform = false
|
|
120
|
+
const cfg = {}
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (!line.trim() || line.trim().startsWith('#')) continue
|
|
123
|
+
if (!/^\s/.test(line)) inPlatform = line.startsWith('platform:')
|
|
124
|
+
else if (inPlatform) {
|
|
125
|
+
const m = line.match(/^\s+(url|token)\s*:\s*(.*)$/)
|
|
126
|
+
if (m) {
|
|
127
|
+
let v = m[2].trim()
|
|
128
|
+
if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) v = v.slice(1, -1)
|
|
129
|
+
if (v) cfg[m[1]] = v
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!cfg.url || !cfg.token) return null
|
|
134
|
+
return cfg
|
|
135
|
+
} catch {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PUSH_TIMEOUT_MS = 5_000
|
|
141
|
+
|
|
142
|
+
// 从文件全文提取指定 qlId 的条目块(## 头到下一个 ## 头/文件尾)。null=未命中。
|
|
143
|
+
// 与平台 quicklog_parser.py 同款切分口径(剥 \r),保证 CLI 推送的 raw_block 与平台解析一致。
|
|
144
|
+
function extractRawBlock(content, qlId) {
|
|
145
|
+
if (!content) return null
|
|
146
|
+
const lines = content.split('\n')
|
|
147
|
+
const start = lines.findIndex(l => l.replace(/\r$/, '').startsWith(`## ${qlId} |`))
|
|
148
|
+
if (start === -1) return null
|
|
149
|
+
let end = lines.length
|
|
150
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
151
|
+
if (lines[i].replace(/\r$/, '').startsWith('## ')) { end = i; break }
|
|
152
|
+
}
|
|
153
|
+
return lines.slice(start, end).map(l => l.replace(/\r$/, '')).join('\n').trimEnd()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 落盘条目块 → 推送 payload(design §5.3:以落盘终态为准,不从入参拼)。
|
|
157
|
+
// 宽松标签解析对齐平台:全半角冒号、多条状态行取最后、文件行单行/多行 bullet。
|
|
158
|
+
function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_changes, fallback_title }) {
|
|
159
|
+
const payload = {
|
|
160
|
+
ql_id,
|
|
161
|
+
timestamp: null,
|
|
162
|
+
title: fallback_title || '(quick 任务)',
|
|
163
|
+
status,
|
|
164
|
+
status_note: null,
|
|
165
|
+
author_raw,
|
|
166
|
+
linked_changes: Array.isArray(linked_changes) ? linked_changes : [],
|
|
167
|
+
files: [],
|
|
168
|
+
body_sections: {},
|
|
169
|
+
raw_block: rawBlock,
|
|
170
|
+
}
|
|
171
|
+
if (!rawBlock) return payload
|
|
172
|
+
const lines = rawBlock.split('\n')
|
|
173
|
+
const header = lines[0] || ''
|
|
174
|
+
const parts = header.split('|').map(s => s.trim())
|
|
175
|
+
if (parts.length >= 3) {
|
|
176
|
+
payload.timestamp = parts[1] || null
|
|
177
|
+
payload.title = parts.slice(2).join('|').trim() || payload.title
|
|
178
|
+
}
|
|
179
|
+
// 顶层标签白名单(^ 行首锚定):D-004@v1 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行)
|
|
180
|
+
// 因「- 」前缀不匹配本正则——它们是根因块正文内的列表行续行,不是新顶层标签,经 lastLabel 挂载
|
|
181
|
+
// 进 body_sections[根因](见下方 inFiles 复位注释)。顶层四字段边界解析不受影响(R-03)。
|
|
182
|
+
const labelRe = /^(状态|关联变更|文件|审计|需求|根因|方案|结果)\s*[::]\s*(.*)$/
|
|
183
|
+
const bulletRe = /^-\s+(.*)$/
|
|
184
|
+
let inFiles = false
|
|
185
|
+
let inLinked = false
|
|
186
|
+
let lastLabel = null
|
|
187
|
+
let lastStatus = null
|
|
188
|
+
for (const line of lines.slice(1)) {
|
|
189
|
+
const stripped = line.trim()
|
|
190
|
+
if (!stripped) { inFiles = false; inLinked = false; continue }
|
|
191
|
+
const m = stripped.match(labelRe)
|
|
192
|
+
if (m) {
|
|
193
|
+
const [, label, value] = m
|
|
194
|
+
if (label === '状态') lastStatus = value
|
|
195
|
+
else if (label === '关联变更') { payload.linked_changes = value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean); inLinked = true; inFiles = false }
|
|
196
|
+
else if (label === '文件') { if (value) payload.files.push(...value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean).map(p => ({ path: p, note: null }))); inFiles = true; inLinked = false }
|
|
197
|
+
else if (label === '审计') { /* D-8 advisory 行:只进 raw_block 不进结构化段(body_sections 不扩 schema),并阻断续行误挂 */ lastLabel = null; inFiles = false; inLinked = false }
|
|
198
|
+
// 进入 需求/根因/方案/结果 字段块须关闭 inFiles/inLinked 续行模式(task-07 / D-004@v1):否则根因块内
|
|
199
|
+
// 嵌套子字段列表行(- 现象:… 等)会命中下方「文件 bullet」分支被劫进 payload.files,从根因正文截断丢失。
|
|
200
|
+
else { payload.body_sections[label] = value; lastLabel = label; inFiles = false; inLinked = false }
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
if (bulletRe.test(stripped) && inFiles) {
|
|
204
|
+
payload.files.push({ path: stripped.replace(/^-\s+/, ''), note: null })
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if (bulletRe.test(stripped) && inLinked) {
|
|
208
|
+
payload.linked_changes.push(...stripped.replace(/^-\s+/, '').split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean))
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
if (lastLabel) payload.body_sections[lastLabel] += '\n' + stripped
|
|
212
|
+
}
|
|
213
|
+
// 状态以落盘为准(flip 后是「已完成」或「已完成(括注)」)——多条取最后
|
|
214
|
+
if (lastStatus) {
|
|
215
|
+
payload.status = lastStatus.startsWith('已完成') ? 'completed'
|
|
216
|
+
: lastStatus.startsWith('已暂存') ? 'partial_done'
|
|
217
|
+
: lastStatus.startsWith('进行中') ? 'in_progress'
|
|
218
|
+
: status
|
|
219
|
+
const note = lastStatus.match(/((.+))$/)
|
|
220
|
+
payload.status_note = note ? note[1] : null
|
|
221
|
+
}
|
|
222
|
+
// 白名单正则对齐平台(^\d{4}-\d{2}-\d{2}- 才进列表,防自由文本进反向区块)
|
|
223
|
+
payload.linked_changes = payload.linked_changes.filter(c => /^\d{4}-\d{2}-\d{2}-/.test(c))
|
|
224
|
+
return payload
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 单条推送:payload 字段对齐平台 QuicklogEntryPushRequest(snake_case)。测试经 allocate/complete 间接触发。
|
|
228
|
+
// 任何失败(无配置/网络/非 2xx/超时)静默 warn 一行不抛(FR-02 best-effort,quick 主流程零阻断)。
|
|
229
|
+
async function pushQuicklogEntryToPlatform(specBase, entry) {
|
|
230
|
+
const cfg = _readPlatformConfig(specBase)
|
|
231
|
+
if (!cfg) {
|
|
232
|
+
if (process.env.SILLYSPEC_DEBUG_SYNC) console.warn('[quicklog-push] 未配置 platform 段,跳过推送')
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
const controller = new AbortController()
|
|
236
|
+
const timer = setTimeout(() => controller.abort(), PUSH_TIMEOUT_MS)
|
|
237
|
+
try {
|
|
238
|
+
const res = await fetch(`${cfg.url.replace(/\/$/, '')}/api/quicklog-entries`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.token}` },
|
|
241
|
+
body: JSON.stringify(entry),
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
})
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
console.warn(`[quicklog-push] ${entry.ql_id} → ${res.status}(文件链路兜底,不影响本地记录)`)
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
return true
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.warn(`[quicklog-push] ${entry.ql_id} 推送失败: ${err && err.name === 'AbortError' ? '超时' : (err.message || err)}(文件链路兜底)`)
|
|
251
|
+
return false
|
|
252
|
+
} finally {
|
|
253
|
+
clearTimeout(timer)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// git user.name → 文件名安全形式:白名单保留字母数字._- 与中文等 Unicode 字,
|
|
258
|
+
// 剥路径元字符(/ \ ..)与控制字符。防 QUICKLOG-<user>.md / 锁文件 / 轮转归档穿越写。
|
|
259
|
+
function sanitizeQuicklogUser(user) {
|
|
260
|
+
const s = String(user || '').replace(/[\\/\r\n\0]/g, '').replace(/\.{2,}/g, '.').trim()
|
|
261
|
+
if (!s) return ''
|
|
262
|
+
return s.length > 64 ? s.slice(0, 64) : s
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 从关联变更的 proposal/design 提取首个 # 标题(去「提案书(Proposal)— / 设计文档(Design)—」固定前缀)。
|
|
266
|
+
// 让 quick --linked-changes 启动时(用户常不带 --input)也能拿到语义标题,而非 (quick 任务) 占位。
|
|
267
|
+
// 读不到任何标题返回 ''(调用方再回退占位符,保持向后兼容)。
|
|
268
|
+
export function deriveTitleFromLinkedChange(specBase, change) {
|
|
269
|
+
if (!change) return ''
|
|
270
|
+
for (const f of ['proposal.md', 'design.md']) {
|
|
271
|
+
let content
|
|
272
|
+
try { content = readFileSync(join(specBase, 'changes', change, f), 'utf8') } catch { continue }
|
|
273
|
+
const m = content.match(/^#\s+(.+?)\s*$/m)
|
|
274
|
+
if (!m) continue
|
|
275
|
+
let raw = m[1].trim()
|
|
276
|
+
// 标题惯例「# 提案书(Proposal)— <desc>」「# 设计文档(Design)— <desc>」→ 取破折号后的 desc。
|
|
277
|
+
// 固定前缀显式剥(坑 linked-task-placeholder-title,2026-08-22 实证:标题「# 提案书(Proposal):
|
|
278
|
+
// xxx」或「# 提案书(Proposal)」无破折号时 dash 匹配失败,整串前缀落进 tasks.md 追加行)——
|
|
279
|
+
// 先剥固定模板词(含全/半角冒号分隔形态),再剥破折号;剥完为空(纯模板标题)继续找下一文档
|
|
280
|
+
for (const prefix of [/^提案书(Proposal)\s*[::—\-]?\s*/, /^设计文档(Design)\s*[::—\-]?\s*/, /^Proposal\s*[::—\-]?\s*/i, /^Design\s*[::—\-]?\s*/i]) {
|
|
281
|
+
raw = raw.replace(prefix, '').trim()
|
|
282
|
+
}
|
|
283
|
+
if (!raw) continue
|
|
284
|
+
const dash = raw.match(/[—-]\s*(.+)$/)
|
|
285
|
+
const title = dash ? dash[1].trim() : raw
|
|
286
|
+
if (title) return title
|
|
287
|
+
}
|
|
288
|
+
return ''
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 从 quick step3 --output 四字段提取「需求:」摘要,用于翻完成时刷新标题行
|
|
292
|
+
// (覆盖启动时的占位/弱标题)。提取失败返回 ''(不刷新)。
|
|
293
|
+
export function extractTitleFromResult(result) {
|
|
294
|
+
if (!result) return ''
|
|
295
|
+
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
296
|
+
if (!m) return ''
|
|
297
|
+
let t = m[1].replace(/[,。;,;].*$/, '').trim() // 截到首个标点,取核心句
|
|
298
|
+
if (!t) return ''
|
|
299
|
+
if (t.length > 80) t = t.slice(0, 80) + '…'
|
|
300
|
+
return t
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sanitizeResult(resultText) {
|
|
304
|
+
// 保留换行:结果块支持字段化结构(需求:/根因:/方案:/结果:...)。多行时 flipEntryInContent
|
|
305
|
+
// 写成字段块(追加在「状态:」行下方),不是单条「结果:<长行>」。仅去首尾空白。
|
|
306
|
+
return String(resultText || '').trim()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── quick step3 --file-notes 旁路通道 ──
|
|
310
|
+
// flag 在 command.js 解析(--file-notes),completeQuicklogEntry 在 quick 收尾时消费。
|
|
311
|
+
// 不经 completeStep → handleQuickStageCompletion → completeQuicklogEntry 三层透传(会扩 3 处签名,
|
|
312
|
+
// 且 quick 收尾路径是多 agent 并发改热点);用 per-process setter,CLI 单进程生命周期内有效,
|
|
313
|
+
// completeQuicklogEntry 读后即清。无 --file-notes 时为 '' → 回退 changedFiles 单行(向后兼容)。
|
|
314
|
+
let _pendingFileNotes = ''
|
|
315
|
+
export function setQuickFileNotes(raw) { _pendingFileNotes = raw == null ? '' : String(raw) }
|
|
316
|
+
export function getQuickFileNotes() { return _pendingFileNotes }
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 解析 --file-notes 原文 → [{path, note}]。
|
|
320
|
+
* 格式:「path1::括注1 || path2::括注2」——`||` 分隔条目,`::` 分隔路径与括注(首个 `::` 为界,
|
|
321
|
+
* 括注内含 `::` 不误切);路径反斜杠归一正斜杠(匹配 git 路径风格)。无 `::` 的段 → note=''。
|
|
322
|
+
* 空 / 全空段 → [](调用方回退 changedFiles 单行)。导出供测试 + 未来直接调用。
|
|
323
|
+
*/
|
|
324
|
+
export function parseFileNotes(raw) {
|
|
325
|
+
if (!raw) return []
|
|
326
|
+
// note 单行化 + 限长:bullet 是「数组元素内嵌 \n」落盘(flipEntryInContent),note 带换行会伪造
|
|
327
|
+
// QUICKLOG 条目结构(\n## ql-… 可伪造后续条目头)。与 sanitizeDesc 同风格。
|
|
328
|
+
const sanitizeNote = (n) => n.replace(/[\r\n]+/g, ' ').trim().slice(0, 200)
|
|
329
|
+
return String(raw)
|
|
330
|
+
.split('||')
|
|
331
|
+
.map((seg) => {
|
|
332
|
+
const s = seg.trim()
|
|
333
|
+
if (!s) return null
|
|
334
|
+
const idx = s.indexOf('::')
|
|
335
|
+
const path = (idx === -1 ? s : s.slice(0, idx)).replace(/\\/g, '/').trim()
|
|
336
|
+
const note = idx === -1 ? '' : sanitizeNote(s.slice(idx + 2))
|
|
337
|
+
return path ? { path, note } : null
|
|
338
|
+
})
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 校验 --file-notes 原文格式(坑 quick-file-notes-format-silent,2026-08-24 用户反馈二期②:
|
|
344
|
+
* 多文件分隔符是 `||` 双竖线,写错成 `|`/`,` 时整段被当成第一个文件的括注静默落盘,只能手工修
|
|
345
|
+
* QUICKLOG)。命令入口 fail-fast 用:每个 `||` 段必须是 `path::括注`(`::` 必填,括注可空);
|
|
346
|
+
* 括注内再出现 `::` 视为疑似漏写 `||` 分隔符。返回问题清单供调用方拼报错。
|
|
347
|
+
* @param {string} raw
|
|
348
|
+
* @returns {{ ok: boolean, problems: Array<{ seg: string, reason: string, hint: string }> }}
|
|
349
|
+
*/
|
|
350
|
+
export function validateFileNotesFormat(raw) {
|
|
351
|
+
const problems = []
|
|
352
|
+
if (!raw) return { ok: true, problems }
|
|
353
|
+
for (const seg of String(raw).split('||')) {
|
|
354
|
+
const s = seg.trim()
|
|
355
|
+
if (!s) continue
|
|
356
|
+
const idx = s.indexOf('::')
|
|
357
|
+
if (idx === -1) {
|
|
358
|
+
problems.push({ seg: s, reason: '缺 `::` 路径/括注分隔符', hint: '格式为 path::括注;不要括注也要写 path::(括注可空)' })
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
const path = s.slice(0, idx).trim()
|
|
362
|
+
if (!path) {
|
|
363
|
+
problems.push({ seg: s, reason: '`::` 前路径为空', hint: '格式为 path::括注' })
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
if (s.slice(idx + 2).includes('::')) {
|
|
367
|
+
problems.push({ seg: s, reason: '括注内出现 `::`', hint: '疑似多文件漏写 `||` 分隔符(双竖线)——多文件写法:path1::括注1 || path2::括注2' })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { ok: problems.length === 0, problems }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
|
|
374
|
+
// 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
|
|
375
|
+
const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* quick step3 结果摘要结构校验(确定性:只查「必填字段是否都在」,不判内容质量——
|
|
379
|
+
* 内容好坏属语义软判定,不归 CLI)。对齐 docs/sillyspec/quick-done-quicklog-duplicate-status-line
|
|
380
|
+
* 的第二诉求:--output 不自动展开丰富格式时,用约束模板 + 结构校验保证 QUICKLOG 记录完整。
|
|
381
|
+
* @param {string} text --done --output 原文
|
|
382
|
+
* @returns {{ ok: boolean, missing: string[] }} ok=true 通过;missing=缺失的字段名列表
|
|
383
|
+
*/
|
|
384
|
+
export function validateQuickResult(text) {
|
|
385
|
+
// 子串包含判定:根因块含嵌套四子字段列表行(- 现象:… 等,task-07 / D-004@v1 合法形态)时,
|
|
386
|
+
// 顶层四标签仍须齐备(模板契约),嵌套行不影响判定(「- 」前缀不构成顶层标签,亦不缺字段)。
|
|
387
|
+
const missing = RESULT_REQUIRED_LABELS.filter(label => !String(text || '').includes(label))
|
|
388
|
+
return { ok: missing.length === 0, missing }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function escapeRe(s) {
|
|
392
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function listQuicklogFiles(quicklogDir) {
|
|
396
|
+
try {
|
|
397
|
+
return readdirSync(quicklogDir).filter(f => f.endsWith('.md') && f.startsWith('QUICKLOG'))
|
|
398
|
+
} catch {
|
|
399
|
+
return []
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 原子写:同目录临时文件 + rename 覆盖。reader 不持锁(agent cat / dashboard 轮询),
|
|
404
|
+
// 若直接 writeFileSync 覆盖整文件,reader 会在 truncate→write 间隙读到空/半截(reader-writer 竞态)。
|
|
405
|
+
// rename 同文件系统内原子(POSIX rename / Win MoveFileEx REPLACE_EXISTING),
|
|
406
|
+
// reader 永远看到完整旧版或完整新版,绝不读半截。
|
|
407
|
+
// Windows 差异:rename 覆盖「正被另一进程读取的目标」会抛 EPERM/EBUSY(POSIX 无此问题)。
|
|
408
|
+
// reader 句柄是瞬时的,故对占用类错误做异步重试+退避;非占用错误立即抛。
|
|
409
|
+
async function writeAtomic(filePath, content, opts = {}) {
|
|
410
|
+
const { retries = 100, baseDelayMs = 5, maxDelayMs = 100 } = opts
|
|
411
|
+
const tmp = join(dirname(filePath), basename(filePath) + '.tmp-' + process.pid + '-' + randomBytes(4).toString('hex')) // 随机段:Windows PID 重用激进,仅 pid 会撞名(对齐 fs-atomic.js 的同款修复)
|
|
412
|
+
writeFileSync(tmp, content) // 同目录 = 同文件系统,rename 才原子(跨 fs 会退化 copy+delete)
|
|
413
|
+
for (let attempt = 0; ; attempt++) {
|
|
414
|
+
try {
|
|
415
|
+
renameSync(tmp, filePath)
|
|
416
|
+
return
|
|
417
|
+
} catch (e) {
|
|
418
|
+
const retryable = e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES'
|
|
419
|
+
if (!retryable || attempt >= retries) {
|
|
420
|
+
try { unlinkSync(tmp) } catch {}
|
|
421
|
+
throw e
|
|
422
|
+
}
|
|
423
|
+
await sleep(Math.min(baseDelayMs * (attempt + 1), maxDelayMs)) // 线性退避,让出 loop
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 扫描所有 QUICKLOG-*.md(含轮转归档)当天最大 NNN + 已用 XXXX 后缀集
|
|
429
|
+
function scanExisting(quicklogDir, today) {
|
|
430
|
+
let maxSeq = 0
|
|
431
|
+
const usedSuffix = new Set()
|
|
432
|
+
const re = /^## ql-(\d{8})-(\d{3})-([0-9a-fA-F]{4})\b/gm
|
|
433
|
+
// E22c:轮转归档按文件名日期过滤——归档名 = QUICKLOG-<user>-<YYYY-MM-DD>.md(最后条目日期),
|
|
434
|
+
// 归档内全部条目 ≤ 名内日期;名内日期早于今天的归档不可能含当日条目,跳过读取
|
|
435
|
+
// (O(全历史归档) → O(当日文件),consumer 已 10 归档文件 756KB 时免全量扫描)。
|
|
436
|
+
const todayDashed = today.length === 8 ? `${today.slice(0, 4)}-${today.slice(4, 6)}-${today.slice(6, 8)}` : today
|
|
437
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
438
|
+
const dm = f.match(/-(\d{4}-\d{2}-\d{2})\.md$/)
|
|
439
|
+
if (dm && dm[1] < todayDashed) continue // 早于今天的归档,必无当日条目
|
|
440
|
+
let content = ''
|
|
441
|
+
try { content = readFileSync(join(quicklogDir, f), 'utf8') } catch { continue }
|
|
442
|
+
let m
|
|
443
|
+
re.lastIndex = 0
|
|
444
|
+
while ((m = re.exec(content)) !== null) {
|
|
445
|
+
if (m[1] === today) {
|
|
446
|
+
maxSeq = Math.max(maxSeq, parseInt(m[2], 10))
|
|
447
|
+
usedSuffix.add(m[3].toLowerCase())
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return { maxSeq, usedSuffix }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// >500 行则轮转:rename QUICKLOG-<user>.md → QUICKLOG-<user>-<最后记录日期>.md
|
|
455
|
+
async function rotateIfNeeded(userFile, gitUser) {
|
|
456
|
+
if (!existsSync(userFile)) return
|
|
457
|
+
const content = readFileSync(userFile, 'utf8')
|
|
458
|
+
const lines = content.split('\n')
|
|
459
|
+
if (lines.length <= 500) return
|
|
460
|
+
let lastDate = null
|
|
461
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
462
|
+
const m = lines[i].match(/^## ql-(\d{8})-/)
|
|
463
|
+
if (m) { lastDate = m[1]; break }
|
|
464
|
+
}
|
|
465
|
+
const dateStr = lastDate
|
|
466
|
+
? `${lastDate.slice(0, 4)}-${lastDate.slice(4, 6)}-${lastDate.slice(6, 8)}`
|
|
467
|
+
: nowDatetime().slice(0, 10)
|
|
468
|
+
const archiveFile = join(dirname(userFile), `QUICKLOG-${sanitizeQuicklogUser(gitUser) || 'unknown'}-${dateStr}.md`)
|
|
469
|
+
// 同日已轮转过:两步各自原子化(append+clear 无法单原子,reader 容忍中间态——见 writeAtomic 注释)
|
|
470
|
+
if (existsSync(archiveFile)) {
|
|
471
|
+
const existing = readFileSync(archiveFile, 'utf8')
|
|
472
|
+
await writeAtomic(archiveFile, existing + content)
|
|
473
|
+
await writeAtomic(userFile, '')
|
|
474
|
+
} else {
|
|
475
|
+
renameSync(userFile, archiveFile)
|
|
476
|
+
}
|
|
477
|
+
// echo 轮转:归档文件是 git 跟踪的新文件,提交时带上(勿漏,否则旧 ql 条目只在本地)
|
|
478
|
+
console.log(`🔄 QUICKLOG 已轮转(>500 行):${basename(userFile)} → ${basename(archiveFile)}(提交时带上归档文件,勿漏)`)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function tasksPath(specBase, change) {
|
|
482
|
+
return join(specBase, 'changes', change, 'tasks.md')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 追加未勾选 task;幂等(已含同 qlId 则跳过)。
|
|
486
|
+
// 关联变更目录不存在(笔误 / 未建 / 仅作标签关联)→ 不 fabricate stub 目录(历史坑 quick-change-phantom:
|
|
487
|
+
// mkdirSync 硬造 changes/<名>/tasks.md,致 quick --done 边界审计自造自拦 BLOCK)。关联仍记入
|
|
488
|
+
// QUICKLOG「关联变更」行(allocateQuicklogEntry 独立写入,不依赖本函数)。
|
|
489
|
+
// 体检 BUG-17:tasks.md 是跨用户共享文件——QUICKLOG 按 <user> 分锁只串行化同用户会话,
|
|
490
|
+
// 用户 A 勾选与用户 B 追加跨用户并发时须按目标文件加锁,否则读-改-写互相覆盖。
|
|
491
|
+
function tasksLockPath(p) {
|
|
492
|
+
return p + '.tasklock'
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function appendTaskCheckbox(specBase, change, qlId, desc) {
|
|
496
|
+
const dir = join(specBase, 'changes', change)
|
|
497
|
+
if (!existsSync(dir)) return
|
|
498
|
+
const p = tasksPath(specBase, change)
|
|
499
|
+
await withFileLock(tasksLockPath(p), () => {
|
|
500
|
+
let content = existsSync(p) ? readFileSync(p, 'utf8') : ''
|
|
501
|
+
if (content.includes(qlId)) return
|
|
502
|
+
const prefix = content && !content.endsWith('\n') ? '\n' : ''
|
|
503
|
+
appendFileSync(p, `${prefix}- [ ] ${qlId} ${desc}\n`)
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 勾选该 qlId 对应 task:- [ ] → - [x](同 BUG-17:按 tasks.md 文件锁串行化跨用户读-改-写)
|
|
508
|
+
async function checkTaskCheckbox(specBase, change, qlId) {
|
|
509
|
+
const p = tasksPath(specBase, change)
|
|
510
|
+
if (!existsSync(p)) return
|
|
511
|
+
await withFileLock(tasksLockPath(p), async () => {
|
|
512
|
+
let content = readFileSync(p, 'utf8')
|
|
513
|
+
const re = new RegExp(`- \\[ \\] (${escapeRe(qlId)})`)
|
|
514
|
+
content = content.replace(re, '- [x] $1')
|
|
515
|
+
await writeAtomic(p, content)
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 就地翻某 qlId 条目:状态进行中→已完成,追加结果行
|
|
520
|
+
// CRLF 修复(缺陷 quick-done-quicklog-duplicate-status-line):QUICKLOG 在 Windows 下
|
|
521
|
+
// 可能是 CRLF,split('\n') 后每行带 \r。原代码 `lines[i] === '状态:进行中'` 精确匹配
|
|
522
|
+
// 恒失败 → 走 splice「兜底插入」→ 条目内同时出现「状态:已完成」+「状态:进行中」。
|
|
523
|
+
// 状态行匹配与「已完成」判断统一用行首前缀匹配,容忍行尾 \r;写入保持 CRLF 不扩大改动。
|
|
524
|
+
// 单行四字段(需求:/根因:/方案:/结果:)切段。先按「字段边界」严格扫描:真实标签 = 上一标签之后首次
|
|
525
|
+
// 出现的、前导是串首/空白/句末标点(。;!?)的对应标签——字段正文引用标签字样(根因写「双层「结果:」
|
|
526
|
+
// 前缀」或正则 split(/(?=需求:|根因:|方案:|结果:)/))因前导是「/|( 等非边界字符而跳过。严格失败
|
|
527
|
+
// (如真实标签前导是「,」这类弱标点)退回宽松顺序扫描(上一标签之后首次出现)。缺标签返回 null → 落单行
|
|
528
|
+
// 兜底(--done 契约校验仍会拦缺字段)。残余边界:正文引用标签且前导恰好是空白/句末标点时仍可能错位。
|
|
529
|
+
// task-07 / D-004@v1 声明:本切分只作用于「单行四字段压缩归一」路径,嵌套四子字段(- 现象:/- 根因:/
|
|
530
|
+
// - 护栏:/- 证据: 列表行)必须以多行列表行形态写入根因块正文——按序扫描先命中顶层「根因:」标签,
|
|
531
|
+
// 其后的嵌套「- 根因:」等字样落在根因字段正文内不构成新边界,无需改动三个边界函数即天然兼容(Grill C-15)。
|
|
532
|
+
function isFieldBoundary(body, idx) {
|
|
533
|
+
if (idx <= 0) return true
|
|
534
|
+
const prev = body[idx - 1]
|
|
535
|
+
return /\s/.test(prev) || '。;!?'.includes(prev)
|
|
536
|
+
}
|
|
537
|
+
function findBoundaryLabel(body, label, from) {
|
|
538
|
+
let idx = body.indexOf(label, from)
|
|
539
|
+
while (idx !== -1 && !isFieldBoundary(body, idx)) {
|
|
540
|
+
idx = body.indexOf(label, idx + label.length)
|
|
541
|
+
}
|
|
542
|
+
return idx
|
|
543
|
+
}
|
|
544
|
+
function scanFields(body, findLabel) {
|
|
545
|
+
const labels = ['需求:', '根因:', '方案:', '结果:']
|
|
546
|
+
const positions = []
|
|
547
|
+
let cursor = 0
|
|
548
|
+
for (const label of labels) {
|
|
549
|
+
const idx = findLabel(body, label, cursor)
|
|
550
|
+
if (idx === -1) return null
|
|
551
|
+
positions.push(idx)
|
|
552
|
+
cursor = idx + label.length
|
|
553
|
+
}
|
|
554
|
+
return positions.map((start, i) => {
|
|
555
|
+
const end = i + 1 < positions.length ? positions[i + 1] : body.length
|
|
556
|
+
return body.slice(start, end).trim()
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
function splitSingleLineFields(body) {
|
|
560
|
+
return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = [], auditNotes = []) {
|
|
564
|
+
const lines = content.split('\n')
|
|
565
|
+
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
566
|
+
if (startIdx === -1) return null
|
|
567
|
+
// 翻完成时若能从结果提取「需求:」摘要,刷新标题行(覆盖启动时的占位/弱标题)
|
|
568
|
+
const newTitle = extractTitleFromResult(result)
|
|
569
|
+
if (newTitle) {
|
|
570
|
+
const headerCore = lines[startIdx].match(/^(## \S+ \| [^|]+ \| )/)
|
|
571
|
+
if (headerCore) lines[startIdx] = headerCore[1] + newTitle
|
|
572
|
+
}
|
|
573
|
+
let endIdx = lines.length
|
|
574
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
575
|
+
if (lines[i].startsWith('## ')) { endIdx = i; break }
|
|
576
|
+
}
|
|
577
|
+
let hasResult = false
|
|
578
|
+
let flipped = false
|
|
579
|
+
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
580
|
+
if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
|
|
581
|
+
if (lines[i].startsWith('结果:')) hasResult = true
|
|
582
|
+
// 文件行落盘:--file-notes 优先(多行 bullet 带括注),否则回填审计到的实际改动文件单行,
|
|
583
|
+
// 都空则不动(保持「(见实际改动)」)。bullet 用「数组元素内嵌 \n」写——lines.join('\n') 展平为
|
|
584
|
+
// 多行,数组长度不变 → 与下方状态/结果 splice 的索引完全解耦(changedFiles 单行同理原地替换)。
|
|
585
|
+
if (lines[i].startsWith('文件:')) {
|
|
586
|
+
if (fileNotes.length > 0) {
|
|
587
|
+
const bullets = fileNotes.map((n) => `- ${n.path}${n.note ? `(${n.note})` : ''}`)
|
|
588
|
+
lines[i] = `文件:\n${bullets.join('\n')}`
|
|
589
|
+
} else if (changedFiles.length > 0) {
|
|
590
|
+
lines[i] = `文件:${changedFiles.join(', ')}`
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
|
|
595
|
+
// 无状态行(异常),插一条已完成
|
|
596
|
+
lines.splice(startIdx + 1, 0, '状态:已完成')
|
|
597
|
+
endIdx += 1
|
|
598
|
+
}
|
|
599
|
+
if (result && !hasResult) {
|
|
600
|
+
// quick step3 --output 常被压成单行「需求:…根因:…方案:…结果:…」(agent 未加换行),
|
|
601
|
+
// 直接落盘会得到双层前缀「结果:需求:…结果:…」。把单行四字段归一为多行字段块,省 agent 手工
|
|
602
|
+
// 拆行精修(prompt-control-debt quick-①)。多行 / 单句结果不受影响。
|
|
603
|
+
let body = result
|
|
604
|
+
if (!/\r?\n/.test(body) && /^需求:/.test(body)) {
|
|
605
|
+
// 单行四字段归一为多行。不能 split(/(?=需求:|根因:|方案:|结果:)/)——正文引用字段标签字样
|
|
606
|
+
// (如根因里写「双层「结果:」前缀」)会被任意位置误切。改按序扫描:真实标签 = 上一标签之后
|
|
607
|
+
// 首次出现,字段正文引用更靠后的标签不误断(quick-① 2026-08-04 实证补丁)。缺标签返回 null
|
|
608
|
+
// → 落单行兜底(--done 契约校验仍会拦缺字段)。
|
|
609
|
+
const segs = splitSingleLineFields(body)
|
|
610
|
+
if (segs) body = segs.join('\n')
|
|
611
|
+
}
|
|
612
|
+
// 多行结果(结构化 需求/根因/方案/结果 字段块):逐行插入为独立字段行。
|
|
613
|
+
// 根因块嵌套四子字段(- 现象:/- 根因:/- 护栏:/- 证据: 列表行,D-004@v1 postmortem 形态)属
|
|
614
|
+
// 根因块正文:在此随多行结果逐行原样落盘(带「- 」前缀、保序含换行),不被字段边界扫描误切——
|
|
615
|
+
// 单行归一切分只认「- 」前缀之外的需求:/根因:/方案:/结果: 标签,嵌套行天然不参与边界判定。
|
|
616
|
+
// 单行结果:保持「结果:<一句话>」一行,向后兼容简单用例。
|
|
617
|
+
const resultLines = /\r?\n/.test(body)
|
|
618
|
+
? body.split(/\r?\n/).filter(l => l.trim() !== '')
|
|
619
|
+
: [`结果:${body}`]
|
|
620
|
+
// 结果块属本条目:从 endIdx 往前跳过本条目尾部空行,在最后一个非空行之后插入。
|
|
621
|
+
// 否则结果块会落在尾空行「之后」,与下一条目标题紧贴(缺空行分隔),且被空行从本条目
|
|
622
|
+
// 「文件:」行隔开——视觉上像属于下一条目(用户实证多条目 QUICKLOG 间距 bug)。
|
|
623
|
+
let insertAt = endIdx
|
|
624
|
+
while (insertAt - 1 > startIdx && lines[insertAt - 1].trim() === '') insertAt--
|
|
625
|
+
lines.splice(insertAt, 0, ...resultLines)
|
|
626
|
+
// 兜底:结果块之后、下一个 ## 标题之间若无空行(条目原本无尾空行 / 轮转归档手改等),补一个。
|
|
627
|
+
const after = insertAt + resultLines.length
|
|
628
|
+
if (after < lines.length && lines[after].startsWith('## ')) {
|
|
629
|
+
lines.splice(after, 0, '')
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// D-8 落盘(2026-08-18 修):advisory 审计行(文档欠账 / 引用失效)写入条目尾部——修复
|
|
633
|
+
// quick-audit「欠账已记录(QUICKLOG reasons)」的不实承诺(原先纯 console 打印,事后不可审计;
|
|
634
|
+
// 两周实测需要分母:信号触发次数必须可追溯)。幂等:条目内已有 审计: 行则不重复写(--done 重跑安全)。
|
|
635
|
+
// 位置:结果块之后 / 条目尾部(重算边界,不吃上方 splice 的陈旧索引)。
|
|
636
|
+
if (auditNotes.length > 0) {
|
|
637
|
+
let entryEnd = -1
|
|
638
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
639
|
+
if (lines[i].startsWith('## ')) { entryEnd = i; break }
|
|
640
|
+
}
|
|
641
|
+
if (entryEnd === -1) entryEnd = lines.length
|
|
642
|
+
if (!lines.slice(startIdx, entryEnd).some((l) => l.startsWith('审计:'))) {
|
|
643
|
+
let insertAt = entryEnd
|
|
644
|
+
while (insertAt - 1 > startIdx && lines[insertAt - 1].trim() === '') insertAt--
|
|
645
|
+
lines.splice(insertAt, 0, ...auditNotes.map((n) => `审计:${n}`))
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return lines.join('\n')
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ── 对外 API ──
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 分配 ql-ID 并写「进行中」条目 + 关联 tasks.md。持锁、当天唯一。
|
|
655
|
+
* @returns {Promise<{qlId: string}>}
|
|
656
|
+
*/
|
|
657
|
+
export async function allocateQuicklogEntry(specBase, gitUser, { description, linkedChanges = [], allowedFiles = [] } = {}) {
|
|
658
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
659
|
+
mkdirSync(quicklogDir, { recursive: true })
|
|
660
|
+
// git user.name 无字符限制,可含 / \ .. 等路径元字符(git config 或 .git/config 可控)——
|
|
661
|
+
// 白名单消毒防穿越写(QUICKLOG-<user>.md / 锁文件 / 轮转归档三处拼接),与 assertSafeChangeName 同风格
|
|
662
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
663
|
+
const userFile = join(quicklogDir, `QUICKLOG-${user}.md`)
|
|
664
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
665
|
+
const desc = sanitizeDesc(description)
|
|
666
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
667
|
+
const files = Array.isArray(allowedFiles) ? allowedFiles : []
|
|
668
|
+
const today = todayStamp()
|
|
669
|
+
|
|
670
|
+
return await withFileLock(lockPath, async () => {
|
|
671
|
+
const { maxSeq, usedSuffix } = scanExisting(quicklogDir, today)
|
|
672
|
+
const nextSeq = maxSeq + 1
|
|
673
|
+
// XXXX 4 位 hex 随机后缀(消歧;NNN 已在锁内顺序分配保证唯一,此处仅 belt-and-suspenders)
|
|
674
|
+
let suffix
|
|
675
|
+
let guard = 0
|
|
676
|
+
do {
|
|
677
|
+
suffix = randomBytes(2).toString('hex')
|
|
678
|
+
guard++
|
|
679
|
+
} while (usedSuffix.has(suffix) && guard < 100)
|
|
680
|
+
|
|
681
|
+
await rotateIfNeeded(userFile, user)
|
|
682
|
+
|
|
683
|
+
const qlId = `ql-${today}-${String(nextSeq).padStart(3, '0')}-${suffix}`
|
|
684
|
+
const entry = [
|
|
685
|
+
'',
|
|
686
|
+
`## ${qlId} | ${nowDatetime()} | ${desc}`,
|
|
687
|
+
'状态:进行中',
|
|
688
|
+
`关联变更:${linked.length > 0 ? linked.join(', ') : '(无)'}`,
|
|
689
|
+
`文件:${files.length > 0 ? files.join(', ') : '(见实际改动)'}`,
|
|
690
|
+
'',
|
|
691
|
+
].join('\n')
|
|
692
|
+
appendFileSync(userFile, entry)
|
|
693
|
+
|
|
694
|
+
for (const c of linked) await appendTaskCheckbox(specBase, c, qlId, desc)
|
|
695
|
+
|
|
696
|
+
// 平台推送(task-06 / FR-02 / D-003):锁外语义等同(推送不回写本地),但放锁内
|
|
697
|
+
// 保证「分配即推送」顺序一致;best-effort 失败不影响返回。
|
|
698
|
+
await pushQuicklogEntryToPlatform(specBase, {
|
|
699
|
+
ql_id: qlId,
|
|
700
|
+
timestamp: nowDatetime(),
|
|
701
|
+
title: desc,
|
|
702
|
+
status: 'in_progress',
|
|
703
|
+
status_note: null,
|
|
704
|
+
author_raw: user,
|
|
705
|
+
// 白名单正则对齐平台(^\d{4}-\d{2}-\d{2}- 才进列表;原文在 raw_block 不丢)
|
|
706
|
+
linked_changes: linked.filter(c => /^\d{4}-\d{2}-\d{2}-/.test(c)),
|
|
707
|
+
files: files.map(f => ({ path: f, note: null })),
|
|
708
|
+
body_sections: {},
|
|
709
|
+
raw_block: entry,
|
|
710
|
+
}).catch(() => {}) // pushQuicklogEntryToPlatform 自身已吞错;双保险防未来改动破坏 best-effort 契约
|
|
711
|
+
|
|
712
|
+
return { qlId }
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
|
|
718
|
+
*/
|
|
719
|
+
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [], auditNotes = [] } = {}) {
|
|
720
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
721
|
+
// 与 allocateQuicklogEntry 同源消毒:锁文件路径含 user(防穿越写,两入口必须一致否则锁不上同一文件)
|
|
722
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
723
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
724
|
+
const result = sanitizeResult(resultText)
|
|
725
|
+
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
726
|
+
// 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
|
|
727
|
+
const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
|
|
728
|
+
// --file-notes(command.js 经 setQuickFileNotes 注入):有则文件行写多行 bullet 带括注。
|
|
729
|
+
// 读后即清(per-process,防残留跨调用)。flipEntryInContent 优先用 fileNotes,空则回退 realFiles。
|
|
730
|
+
const fileNotes = parseFileNotes(_pendingFileNotes)
|
|
731
|
+
_pendingFileNotes = ''
|
|
732
|
+
|
|
733
|
+
await withFileLock(lockPath, async () => {
|
|
734
|
+
// 条目可能在主文件或轮转归档中
|
|
735
|
+
let updatedContent = null
|
|
736
|
+
let updatedFile = null
|
|
737
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
738
|
+
const filePath = join(quicklogDir, f)
|
|
739
|
+
let content = ''
|
|
740
|
+
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
741
|
+
const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes,
|
|
742
|
+
Array.isArray(auditNotes) ? auditNotes.filter((n) => typeof n === 'string' && n.trim() !== '') : [])
|
|
743
|
+
if (updated !== null) {
|
|
744
|
+
await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
|
|
745
|
+
updatedContent = updated
|
|
746
|
+
updatedFile = filePath
|
|
747
|
+
break
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
for (const c of linked) await checkTaskCheckbox(specBase, c, qlId)
|
|
751
|
+
|
|
752
|
+
// 平台推送(task-06 / FR-02 / D-003):**以落盘终态为准**读回条目组装 payload
|
|
753
|
+
// (design §5.3:翻完成时标题行被 extractTitleFromResult 刷新,推送须与落盘一致,
|
|
754
|
+
// 不能用入参拼——标题/结果块/文件行都会在 flipEntryInContent 中重写)。
|
|
755
|
+
const rawBlock = extractRawBlock(updatedContent, qlId)
|
|
756
|
+
await pushQuicklogEntryToPlatform(specBase, buildPushPayloadFromRaw(rawBlock, {
|
|
757
|
+
ql_id: qlId,
|
|
758
|
+
author_raw: user,
|
|
759
|
+
status: 'completed',
|
|
760
|
+
linked_changes: linked,
|
|
761
|
+
})).catch(() => {})
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* 查某 qlId 条目是否存在(只读,跨所有 QUICKLOG-*.md)。
|
|
767
|
+
*/
|
|
768
|
+
export function findQuicklogEntry(specBase, gitUser, qlId) {
|
|
769
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
770
|
+
if (!existsSync(quicklogDir)) return false
|
|
771
|
+
const header = `## ${qlId} |`
|
|
772
|
+
for (const f of listQuicklogFiles(quicklogDir)) {
|
|
773
|
+
let content = ''
|
|
774
|
+
try { content = readFileSync(join(quicklogDir, f), 'utf8') } catch { continue }
|
|
775
|
+
if (content.includes(header)) return true
|
|
776
|
+
}
|
|
777
|
+
return false
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* 取消一个 quick 会话(2026-08-21 agent-手工产出审计第四批 C-2,`run quick --cancel` 数据源)。
|
|
782
|
+
*
|
|
783
|
+
* 误启动/放弃的 quick 会话此前无取消出口:QUICKLOG「进行中」条目 + 关联变更 tasks.md 挂载行
|
|
784
|
+
* + .runtime/quick-sessions/<id>/ 永久残留,doctor --cleanup-ghosts 只清 db 幽灵行不清这三处。
|
|
785
|
+
* 本函数做 allocateQuicklogEntry 的反操作(三步全是已有函数的反面):
|
|
786
|
+
* 1. QUICKLOG 条目状态翻「已取消」(非删除——保留痕迹可审计,与 doctor 宁漏勿杀口径一致)
|
|
787
|
+
* 2. tasks.md 里该 qlId 的挂载行整行移除(未勾的空壳行,勾了说明实际做了工作 → 拒绝取消)
|
|
788
|
+
* 3. 会话 guard 目录删除(quick-sessions/<sessionId>/,含 guard.json)
|
|
789
|
+
* 4. current-quick-run-id 若指向本会话则删除(防后续 --done fallback 命中死会话)
|
|
790
|
+
* db 侧 change 行由调用方 unregisterChange(quick 会话行,非真实变更)。
|
|
791
|
+
*
|
|
792
|
+
* fail-closed:已翻「已完成」的条目拒绝取消(那是真实工作);tasks.md 挂载行已勾选同样拒绝。
|
|
793
|
+
*
|
|
794
|
+
* @param {{ specBase: string, gitUser: string, qlId: string, sessionId: string|null }} opts
|
|
795
|
+
* @returns {Promise<{ ok: boolean, reason?: string, quicklogFile?: string, removedTaskRows: string[] }>}
|
|
796
|
+
*/
|
|
797
|
+
export async function cancelQuickSession({ specBase, gitUser, qlId, sessionId = null }) {
|
|
798
|
+
if (!qlId) return { ok: false, reason: '缺 qlId(--change <quick-会话ID> 或 --ql <ql-xxx>)' }
|
|
799
|
+
const quicklogDir = join(specBase, 'quicklog')
|
|
800
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
801
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
802
|
+
|
|
803
|
+
// 1. QUICKLOG 翻「已取消」:找到条目,校验未完成,就地改状态行
|
|
804
|
+
let quicklogFile = null
|
|
805
|
+
let flipped = false
|
|
806
|
+
await withFileLock(lockPath, async () => {
|
|
807
|
+
// 用户文件缺失时扫全部 QUICKLOG 文件(条目可能在别的 user 文件里,如 CI 场景 user 漂移)
|
|
808
|
+
const files = existsSync(join(quicklogDir, `QUICKLOG-${user}.md`))
|
|
809
|
+
? [`QUICKLOG-${user}.md`]
|
|
810
|
+
: listQuicklogFiles(quicklogDir)
|
|
811
|
+
for (const f of files) {
|
|
812
|
+
const p = join(quicklogDir, f)
|
|
813
|
+
const content = existsSync(p) ? readFileSync(p, 'utf8') : ''
|
|
814
|
+
if (!content.includes(`## ${qlId} |`)) continue
|
|
815
|
+
quicklogFile = p
|
|
816
|
+
const lines = content.split('\n')
|
|
817
|
+
const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
|
|
818
|
+
let endIdx = lines.length
|
|
819
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
820
|
+
if (lines[i].startsWith('## ')) { endIdx = i; break }
|
|
821
|
+
}
|
|
822
|
+
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
823
|
+
if (/^状态:已完成/.test(lines[i])) {
|
|
824
|
+
throw new Error(`${qlId} 已完成(真实工作记录),拒绝取消;如需修正请手改 QUICKLOG`)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (let i = startIdx + 1; i < endIdx; i++) {
|
|
828
|
+
if (/^状态:进行中/.test(lines[i])) {
|
|
829
|
+
lines[i] = '状态:已取消'
|
|
830
|
+
flipped = true
|
|
831
|
+
break
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (flipped) {
|
|
835
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n'
|
|
836
|
+
writeFileSync(p, lines.join(eol === '\r\n' ? '\r\n' : '\n'))
|
|
837
|
+
}
|
|
838
|
+
break
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
if (!quicklogFile) return { ok: false, reason: `未在任何 QUICKLOG 文件找到条目 ${qlId}` }
|
|
842
|
+
if (!flipped) return { ok: false, reason: `${qlId} 状态行异常(非进行中也非已完成),请手查 QUICKLOG` }
|
|
843
|
+
|
|
844
|
+
// 2. tasks.md 挂载行移除(所有活跃变更统一扫——allocate 时可能挂多个关联变更)
|
|
845
|
+
const removedTaskRows = []
|
|
846
|
+
const changesRoot = join(specBase, 'changes')
|
|
847
|
+
if (existsSync(changesRoot)) {
|
|
848
|
+
for (const c of readdirSync(changesRoot, { withFileTypes: true })) {
|
|
849
|
+
if (!c.isDirectory() || c.name === 'archive') continue
|
|
850
|
+
const tp = tasksPath(specBase, c.name)
|
|
851
|
+
if (!existsSync(tp)) continue
|
|
852
|
+
await withFileLock(tasksLockPath(tp), () => {
|
|
853
|
+
const content = readFileSync(tp, 'utf8')
|
|
854
|
+
if (!content.includes(qlId)) return
|
|
855
|
+
const lines = content.split('\n')
|
|
856
|
+
const kept = lines.filter(l => {
|
|
857
|
+
const m = l.match(/^[-*]\s*\[([ xX])\]\s*(\S+)/)
|
|
858
|
+
if (m && m[2] === qlId) {
|
|
859
|
+
if (m[1] !== ' ') throw new Error(`${c.name}/tasks.md 的 ${qlId} 行已勾选(实际做了工作),拒绝取消`)
|
|
860
|
+
removedTaskRows.push(`${c.name}/tasks.md`)
|
|
861
|
+
return false
|
|
862
|
+
}
|
|
863
|
+
return true
|
|
864
|
+
})
|
|
865
|
+
if (kept.length !== lines.length) {
|
|
866
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n'
|
|
867
|
+
writeFileSync(tp, kept.join(eol === '\r\n' ? '\r\n' : '\n'))
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// 3+4. 会话目录与 current marker
|
|
874
|
+
if (sessionId) {
|
|
875
|
+
try { rmSync(join(specBase, '.runtime', 'quick-sessions', sessionId), { recursive: true, force: true }) } catch {}
|
|
876
|
+
const marker = join(specBase, '.runtime', 'current-quick-run-id')
|
|
877
|
+
try {
|
|
878
|
+
if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === sessionId) unlinkSync(marker)
|
|
879
|
+
} catch {}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return { ok: true, quicklogFile, removedTaskRows }
|
|
883
|
+
}
|