gsd-pi 2.41.0-dev.0acbce9 → 2.41.0-dev.5a170d0
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/README.md +1 -1
- package/dist/cli-web-branch.d.ts +6 -0
- package/dist/cli-web-branch.js +17 -0
- package/dist/onboarding.js +2 -1
- package/dist/resources/extensions/gsd/auto/loop.js +89 -1
- package/dist/resources/extensions/gsd/auto/phases.js +28 -10
- package/dist/resources/extensions/gsd/auto/session.js +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +8 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
- package/dist/resources/extensions/gsd/auto-start.js +8 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
- package/dist/resources/extensions/gsd/auto.js +64 -2
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +40 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
- package/dist/resources/extensions/gsd/context-injector.js +74 -0
- package/dist/resources/extensions/gsd/context-store.js +4 -3
- package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
- package/dist/resources/extensions/gsd/custom-verification.js +145 -0
- package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
- package/dist/resources/extensions/gsd/db-writer.js +5 -2
- package/dist/resources/extensions/gsd/definition-loader.js +352 -0
- package/dist/resources/extensions/gsd/detection.js +1 -1
- package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
- package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
- package/dist/resources/extensions/gsd/doctor.js +11 -1
- package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
- package/dist/resources/extensions/gsd/engine-types.js +8 -0
- package/dist/resources/extensions/gsd/execution-policy.js +8 -0
- package/dist/resources/extensions/gsd/exit-command.js +12 -2
- package/dist/resources/extensions/gsd/export.js +9 -13
- package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
- package/dist/resources/extensions/gsd/files.js +28 -11
- package/dist/resources/extensions/gsd/forensics.js +10 -3
- package/dist/resources/extensions/gsd/git-service.js +5 -1
- package/dist/resources/extensions/gsd/graph.js +225 -0
- package/dist/resources/extensions/gsd/gsd-db.js +25 -8
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +7 -3
- package/dist/resources/extensions/gsd/journal.js +85 -0
- package/dist/resources/extensions/gsd/md-importer.js +5 -0
- package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
- package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences.js +1 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/repo-identity.js +46 -2
- package/dist/resources/extensions/gsd/rule-registry.js +489 -0
- package/dist/resources/extensions/gsd/rule-types.js +6 -0
- package/dist/resources/extensions/gsd/run-manager.js +134 -0
- package/dist/resources/extensions/gsd/service-tier.js +138 -0
- package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
- package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
- package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
- package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
- package/dist/resources/extensions/subagent/index.js +7 -3
- package/dist/resources/extensions/voice/index.js +4 -4
- package/dist/resources/skills/create-workflow/SKILL.md +103 -0
- package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/chunks/229.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
- package/dist/web-mode.d.ts +2 -0
- package/dist/web-mode.js +29 -7
- package/package.json +1 -1
- package/packages/native/src/__tests__/text.test.mjs +33 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
- package/src/resources/extensions/gsd/auto/loop.ts +101 -1
- package/src/resources/extensions/gsd/auto/phases.ts +30 -10
- package/src/resources/extensions/gsd/auto/session.ts +6 -0
- package/src/resources/extensions/gsd/auto/types.ts +4 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +9 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
- package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
- package/src/resources/extensions/gsd/auto-start.ts +8 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
- package/src/resources/extensions/gsd/auto.ts +71 -2
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
- package/src/resources/extensions/gsd/commands/catalog.ts +40 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
- package/src/resources/extensions/gsd/context-injector.ts +100 -0
- package/src/resources/extensions/gsd/context-store.ts +4 -3
- package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
- package/src/resources/extensions/gsd/custom-verification.ts +180 -0
- package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
- package/src/resources/extensions/gsd/db-writer.ts +6 -2
- package/src/resources/extensions/gsd/definition-loader.ts +462 -0
- package/src/resources/extensions/gsd/detection.ts +1 -1
- package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
- package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
- package/src/resources/extensions/gsd/doctor.ts +12 -1
- package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
- package/src/resources/extensions/gsd/engine-types.ts +71 -0
- package/src/resources/extensions/gsd/execution-policy.ts +43 -0
- package/src/resources/extensions/gsd/exit-command.ts +14 -2
- package/src/resources/extensions/gsd/export.ts +8 -15
- package/src/resources/extensions/gsd/extension-manifest.json +2 -2
- package/src/resources/extensions/gsd/files.ts +29 -12
- package/src/resources/extensions/gsd/forensics.ts +9 -3
- package/src/resources/extensions/gsd/git-service.ts +5 -4
- package/src/resources/extensions/gsd/graph.ts +312 -0
- package/src/resources/extensions/gsd/gsd-db.ts +37 -8
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +7 -3
- package/src/resources/extensions/gsd/journal.ts +134 -0
- package/src/resources/extensions/gsd/md-importer.ts +6 -0
- package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
- package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
- package/src/resources/extensions/gsd/preferences-types.ts +3 -0
- package/src/resources/extensions/gsd/preferences.ts +1 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/repo-identity.ts +47 -2
- package/src/resources/extensions/gsd/rule-registry.ts +599 -0
- package/src/resources/extensions/gsd/rule-types.ts +68 -0
- package/src/resources/extensions/gsd/run-manager.ts +180 -0
- package/src/resources/extensions/gsd/service-tier.ts +171 -0
- package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
- package/src/resources/extensions/gsd/templates/decisions.md +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +103 -120
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
- package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
- package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
- package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
- package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
- package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
- package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
- package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
- package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
- package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
- package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
- package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
- package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
- package/src/resources/extensions/gsd/types.ts +3 -0
- package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
- package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
- package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
- package/src/resources/extensions/subagent/index.ts +7 -3
- package/src/resources/extensions/voice/index.ts +4 -4
- package/src/resources/skills/create-workflow/SKILL.md +103 -0
- package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
- package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
- package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
- package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
- package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
- package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
- package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
- package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
- package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
|
@@ -9,7 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
10
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
13
|
const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
|
|
14
14
|
function isRepoMeta(value) {
|
|
15
15
|
if (!value || typeof value !== "object")
|
|
@@ -81,6 +81,49 @@ export function readRepoMeta(externalPath) {
|
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
// ─── Inherited-Repo Detection ───────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Check whether `basePath` is inheriting a parent directory's git repo
|
|
87
|
+
* rather than being the git root itself.
|
|
88
|
+
*
|
|
89
|
+
* Returns true when ALL of:
|
|
90
|
+
* 1. basePath is inside a git repo (git rev-parse succeeds)
|
|
91
|
+
* 2. The resolved git root is a proper ancestor of basePath
|
|
92
|
+
* 3. There is no `.gsd` directory at the git root (the parent project
|
|
93
|
+
* has not been initialised with GSD)
|
|
94
|
+
*
|
|
95
|
+
* When true, the caller should run `git init` at basePath so that
|
|
96
|
+
* `repoIdentity()` produces a hash unique to this directory, preventing
|
|
97
|
+
* cross-project state leaks (#1639).
|
|
98
|
+
*
|
|
99
|
+
* When the git root already has `.gsd`, the directory is a legitimate
|
|
100
|
+
* subdirectory of an existing GSD project — `cd src/ && /gsd` should
|
|
101
|
+
* still load the parent project's milestones.
|
|
102
|
+
*/
|
|
103
|
+
export function isInheritedRepo(basePath) {
|
|
104
|
+
try {
|
|
105
|
+
const root = resolveGitRoot(basePath);
|
|
106
|
+
const normalizedBase = canonicalizeExistingPath(basePath);
|
|
107
|
+
const normalizedRoot = canonicalizeExistingPath(root);
|
|
108
|
+
if (normalizedBase === normalizedRoot)
|
|
109
|
+
return false; // basePath IS the root
|
|
110
|
+
// The git root is a proper ancestor. Check whether it already has .gsd
|
|
111
|
+
// (i.e. the parent project was initialised with GSD).
|
|
112
|
+
if (existsSync(join(root, ".gsd")))
|
|
113
|
+
return false;
|
|
114
|
+
// Also walk up from basePath to the git root checking for .gsd
|
|
115
|
+
let dir = normalizedBase;
|
|
116
|
+
while (dir !== normalizedRoot && dir !== dirname(dir)) {
|
|
117
|
+
if (existsSync(join(dir, ".gsd")))
|
|
118
|
+
return false;
|
|
119
|
+
dir = dirname(dir);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
84
127
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
85
128
|
/**
|
|
86
129
|
* Get the git remote URL for "origin", or "" if no remote is configured.
|
|
@@ -105,7 +148,8 @@ function getRemoteUrl(basePath) {
|
|
|
105
148
|
*/
|
|
106
149
|
function canonicalizeExistingPath(path) {
|
|
107
150
|
try {
|
|
108
|
-
|
|
151
|
+
// Use native realpath on Windows to resolve 8.3 short paths (e.g. RUNNER~1)
|
|
152
|
+
return process.platform === "win32" ? realpathSync.native(path) : realpathSync(path);
|
|
109
153
|
}
|
|
110
154
|
catch {
|
|
111
155
|
return resolve(path);
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// GSD Extension — Unified Rule Registry
|
|
2
|
+
//
|
|
3
|
+
// Holds all dispatch rules and hooks as a flat list of UnifiedRule objects.
|
|
4
|
+
// Provides evaluation methods for each phase (dispatch, post-unit, pre-dispatch)
|
|
5
|
+
// and encapsulates mutable hook state as instance fields.
|
|
6
|
+
//
|
|
7
|
+
// A module-level singleton accessor allows existing code to migrate incrementally.
|
|
8
|
+
import { resolvePostUnitHooks, resolvePreDispatchHooks } from "./preferences.js";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
// ─── Artifact Path Resolution ──────────────────────────────────────────────
|
|
12
|
+
export function resolveHookArtifactPath(basePath, unitId, artifactName) {
|
|
13
|
+
const parts = unitId.split("/");
|
|
14
|
+
if (parts.length === 3) {
|
|
15
|
+
const [mid, sid, tid] = parts;
|
|
16
|
+
return join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks", `${tid}-${artifactName}`);
|
|
17
|
+
}
|
|
18
|
+
if (parts.length === 2) {
|
|
19
|
+
const [mid, sid] = parts;
|
|
20
|
+
return join(basePath, ".gsd", "milestones", mid, "slices", sid, artifactName);
|
|
21
|
+
}
|
|
22
|
+
return join(basePath, ".gsd", "milestones", parts[0], artifactName);
|
|
23
|
+
}
|
|
24
|
+
// ─── Dispatch Rule Conversion ──────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Convert an array of DispatchRule objects to UnifiedRule[] format.
|
|
27
|
+
* Preserves exact array order — dispatch is order-dependent (first-match-wins).
|
|
28
|
+
*/
|
|
29
|
+
export function convertDispatchRules(rules) {
|
|
30
|
+
return rules.map((rule) => ({
|
|
31
|
+
name: rule.name,
|
|
32
|
+
when: "dispatch",
|
|
33
|
+
evaluation: "first-match",
|
|
34
|
+
where: rule.match,
|
|
35
|
+
then: (result) => result,
|
|
36
|
+
description: `Dispatch rule: ${rule.name}`,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
// ─── RuleRegistry ─────────────────────────────────────────────────────────
|
|
40
|
+
const HOOK_STATE_FILE = "hook-state.json";
|
|
41
|
+
export class RuleRegistry {
|
|
42
|
+
/** Static dispatch rules provided at construction time. */
|
|
43
|
+
dispatchRules;
|
|
44
|
+
// ── Mutable hook state (encapsulated, not module-level) ──────────────
|
|
45
|
+
activeHook = null;
|
|
46
|
+
hookQueue = [];
|
|
47
|
+
cycleCounts = new Map();
|
|
48
|
+
retryPending = false;
|
|
49
|
+
retryTrigger = null;
|
|
50
|
+
constructor(dispatchRules) {
|
|
51
|
+
this.dispatchRules = dispatchRules;
|
|
52
|
+
}
|
|
53
|
+
// ── Core query ───────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Returns all rules: static dispatch rules + dynamically loaded hook rules.
|
|
56
|
+
* Hook rules are loaded fresh from preferences on each call (not cached).
|
|
57
|
+
*/
|
|
58
|
+
listRules() {
|
|
59
|
+
const rules = [...this.dispatchRules];
|
|
60
|
+
// Convert post-unit hooks to unified rules
|
|
61
|
+
const postHooks = resolvePostUnitHooks();
|
|
62
|
+
for (const hook of postHooks) {
|
|
63
|
+
rules.push({
|
|
64
|
+
name: hook.name,
|
|
65
|
+
when: "post-unit",
|
|
66
|
+
evaluation: "all-matching",
|
|
67
|
+
where: (unitType) => hook.after.includes(unitType),
|
|
68
|
+
then: () => hook,
|
|
69
|
+
description: `Post-unit hook: fires after ${hook.after.join(", ")}`,
|
|
70
|
+
lifecycle: {
|
|
71
|
+
artifact: hook.artifact,
|
|
72
|
+
retry_on: hook.retry_on,
|
|
73
|
+
max_cycles: hook.max_cycles,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Convert pre-dispatch hooks to unified rules
|
|
78
|
+
const preHooks = resolvePreDispatchHooks();
|
|
79
|
+
for (const hook of preHooks) {
|
|
80
|
+
rules.push({
|
|
81
|
+
name: hook.name,
|
|
82
|
+
when: "pre-dispatch",
|
|
83
|
+
evaluation: "all-matching",
|
|
84
|
+
where: (unitType) => hook.before.includes(unitType),
|
|
85
|
+
then: () => hook,
|
|
86
|
+
description: `Pre-dispatch hook: fires before ${hook.before.join(", ")}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return rules;
|
|
90
|
+
}
|
|
91
|
+
// ── Dispatch evaluation (async, first-match-wins) ───────────────────
|
|
92
|
+
/**
|
|
93
|
+
* Iterate dispatch rules in order. First match wins.
|
|
94
|
+
* Returns stop action if no rule matches (unhandled phase).
|
|
95
|
+
*/
|
|
96
|
+
async evaluateDispatch(ctx) {
|
|
97
|
+
for (const rule of this.dispatchRules) {
|
|
98
|
+
const result = await rule.where(ctx);
|
|
99
|
+
if (result) {
|
|
100
|
+
if (result.action !== "skip")
|
|
101
|
+
result.matchedRule = rule.name;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
action: "stop",
|
|
107
|
+
reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
|
|
108
|
+
level: "info",
|
|
109
|
+
matchedRule: "<no-match>",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// ── Post-unit hook evaluation (sync, all-matching with lifecycle) ────
|
|
113
|
+
/**
|
|
114
|
+
* Replicate exact semantics of checkPostUnitHooks from post-unit-hooks.ts:
|
|
115
|
+
* hook-on-hook prevention, idempotency, cycle limits, retry_on, dequeue.
|
|
116
|
+
*/
|
|
117
|
+
evaluatePostUnit(completedUnitType, completedUnitId, basePath) {
|
|
118
|
+
// If we just completed a hook unit, handle its result
|
|
119
|
+
if (this.activeHook) {
|
|
120
|
+
return this._handleHookCompletion(basePath);
|
|
121
|
+
}
|
|
122
|
+
// Don't trigger hooks for other hook units (prevent hook-on-hook chains)
|
|
123
|
+
// Don't trigger hooks for triage units or quick-task units
|
|
124
|
+
if (completedUnitType.startsWith("hook/") ||
|
|
125
|
+
completedUnitType === "triage-captures" ||
|
|
126
|
+
completedUnitType === "quick-task") {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// Check if any hooks are configured for this unit type
|
|
130
|
+
const hooks = resolvePostUnitHooks().filter(h => h.after.includes(completedUnitType));
|
|
131
|
+
if (hooks.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
// Build hook queue for this trigger
|
|
134
|
+
this.hookQueue = hooks.map(config => ({
|
|
135
|
+
config,
|
|
136
|
+
triggerUnitType: completedUnitType,
|
|
137
|
+
triggerUnitId: completedUnitId,
|
|
138
|
+
}));
|
|
139
|
+
return this._dequeueNextHook(basePath);
|
|
140
|
+
}
|
|
141
|
+
_dequeueNextHook(basePath) {
|
|
142
|
+
while (this.hookQueue.length > 0) {
|
|
143
|
+
const entry = this.hookQueue.shift();
|
|
144
|
+
const { config, triggerUnitType, triggerUnitId } = entry;
|
|
145
|
+
// Check idempotency — if artifact already exists, skip
|
|
146
|
+
if (config.artifact) {
|
|
147
|
+
const artifactPath = resolveHookArtifactPath(basePath, triggerUnitId, config.artifact);
|
|
148
|
+
if (existsSync(artifactPath))
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Check cycle limit
|
|
152
|
+
const cycleKey = `${config.name}/${triggerUnitType}/${triggerUnitId}`;
|
|
153
|
+
const currentCycle = (this.cycleCounts.get(cycleKey) ?? 0) + 1;
|
|
154
|
+
const maxCycles = config.max_cycles ?? 1;
|
|
155
|
+
if (currentCycle > maxCycles)
|
|
156
|
+
continue;
|
|
157
|
+
this.cycleCounts.set(cycleKey, currentCycle);
|
|
158
|
+
this.activeHook = {
|
|
159
|
+
hookName: config.name,
|
|
160
|
+
triggerUnitType,
|
|
161
|
+
triggerUnitId,
|
|
162
|
+
cycle: currentCycle,
|
|
163
|
+
pendingRetry: false,
|
|
164
|
+
};
|
|
165
|
+
// Build prompt with variable substitution
|
|
166
|
+
const [mid, sid, tid] = triggerUnitId.split("/");
|
|
167
|
+
let prompt = config.prompt
|
|
168
|
+
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
169
|
+
.replace(/\{sliceId\}/g, sid ?? "")
|
|
170
|
+
.replace(/\{taskId\}/g, tid ?? "");
|
|
171
|
+
// Inject browser safety instruction
|
|
172
|
+
prompt += "\n\n**Browser tool safety:** Do NOT use `browser_wait_for` with `condition: \"network_idle\"` — it hangs indefinitely when dev servers keep persistent connections (Vite HMR, WebSocket). Use `selector_visible`, `text_visible`, or `delay` instead.";
|
|
173
|
+
return {
|
|
174
|
+
hookName: config.name,
|
|
175
|
+
prompt,
|
|
176
|
+
model: config.model,
|
|
177
|
+
unitType: `hook/${config.name}`,
|
|
178
|
+
unitId: triggerUnitId,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// No more hooks — clear active state
|
|
182
|
+
this.activeHook = null;
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
_handleHookCompletion(basePath) {
|
|
186
|
+
const hook = this.activeHook;
|
|
187
|
+
const hooks = resolvePostUnitHooks();
|
|
188
|
+
const config = hooks.find(h => h.name === hook.hookName);
|
|
189
|
+
// Check if retry was requested via retry_on artifact
|
|
190
|
+
if (config?.retry_on) {
|
|
191
|
+
const retryArtifactPath = resolveHookArtifactPath(basePath, hook.triggerUnitId, config.retry_on);
|
|
192
|
+
if (existsSync(retryArtifactPath)) {
|
|
193
|
+
const cycleKey = `${config.name}/${hook.triggerUnitType}/${hook.triggerUnitId}`;
|
|
194
|
+
const currentCycle = this.cycleCounts.get(cycleKey) ?? 1;
|
|
195
|
+
const maxCycles = config.max_cycles ?? 1;
|
|
196
|
+
if (currentCycle < maxCycles) {
|
|
197
|
+
this.activeHook = null;
|
|
198
|
+
this.hookQueue = [];
|
|
199
|
+
this.retryPending = true;
|
|
200
|
+
this.retryTrigger = {
|
|
201
|
+
unitType: hook.triggerUnitType,
|
|
202
|
+
unitId: hook.triggerUnitId,
|
|
203
|
+
retryArtifact: config.retry_on,
|
|
204
|
+
};
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Hook completed normally — try next hook in queue
|
|
210
|
+
this.activeHook = null;
|
|
211
|
+
return this._dequeueNextHook(basePath);
|
|
212
|
+
}
|
|
213
|
+
// ── Pre-dispatch hook evaluation (sync, all-matching with compose) ──
|
|
214
|
+
/**
|
|
215
|
+
* Replicate exact semantics of runPreDispatchHooks from post-unit-hooks.ts:
|
|
216
|
+
* modify/skip/replace compose semantics.
|
|
217
|
+
*/
|
|
218
|
+
evaluatePreDispatch(unitType, unitId, prompt, basePath) {
|
|
219
|
+
// Don't intercept hook units
|
|
220
|
+
if (unitType.startsWith("hook/")) {
|
|
221
|
+
return { action: "proceed", prompt, firedHooks: [] };
|
|
222
|
+
}
|
|
223
|
+
const hooks = resolvePreDispatchHooks().filter(h => h.before.includes(unitType));
|
|
224
|
+
if (hooks.length === 0) {
|
|
225
|
+
return { action: "proceed", prompt, firedHooks: [] };
|
|
226
|
+
}
|
|
227
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
228
|
+
const substitute = (text) => text
|
|
229
|
+
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
230
|
+
.replace(/\{sliceId\}/g, sid ?? "")
|
|
231
|
+
.replace(/\{taskId\}/g, tid ?? "");
|
|
232
|
+
const firedHooks = [];
|
|
233
|
+
let currentPrompt = prompt;
|
|
234
|
+
for (const hook of hooks) {
|
|
235
|
+
if (hook.action === "skip") {
|
|
236
|
+
if (hook.skip_if) {
|
|
237
|
+
const conditionPath = resolveHookArtifactPath(basePath, unitId, hook.skip_if);
|
|
238
|
+
if (!existsSync(conditionPath))
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
firedHooks.push(hook.name);
|
|
242
|
+
return { action: "skip", firedHooks };
|
|
243
|
+
}
|
|
244
|
+
if (hook.action === "replace") {
|
|
245
|
+
firedHooks.push(hook.name);
|
|
246
|
+
return {
|
|
247
|
+
action: "replace",
|
|
248
|
+
prompt: substitute(hook.prompt ?? ""),
|
|
249
|
+
unitType: hook.unit_type,
|
|
250
|
+
model: hook.model,
|
|
251
|
+
firedHooks,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (hook.action === "modify") {
|
|
255
|
+
firedHooks.push(hook.name);
|
|
256
|
+
if (hook.prepend) {
|
|
257
|
+
currentPrompt = `${substitute(hook.prepend)}\n\n${currentPrompt}`;
|
|
258
|
+
}
|
|
259
|
+
if (hook.append) {
|
|
260
|
+
currentPrompt = `${currentPrompt}\n\n${substitute(hook.append)}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
action: "proceed",
|
|
266
|
+
prompt: currentPrompt,
|
|
267
|
+
model: hooks.find(h => h.action === "modify" && h.model)?.model,
|
|
268
|
+
firedHooks,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// ── State accessors ─────────────────────────────────────────────────
|
|
272
|
+
getActiveHook() {
|
|
273
|
+
return this.activeHook;
|
|
274
|
+
}
|
|
275
|
+
isRetryPending() {
|
|
276
|
+
return this.retryPending;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Returns the trigger unit info for a pending retry, or null.
|
|
280
|
+
* Clears the retry state after reading.
|
|
281
|
+
*/
|
|
282
|
+
consumeRetryTrigger() {
|
|
283
|
+
if (!this.retryPending || !this.retryTrigger)
|
|
284
|
+
return null;
|
|
285
|
+
const trigger = { ...this.retryTrigger };
|
|
286
|
+
this.retryPending = false;
|
|
287
|
+
this.retryTrigger = null;
|
|
288
|
+
return trigger;
|
|
289
|
+
}
|
|
290
|
+
/** Clear all mutable state (activeHook, hookQueue, cycleCounts, retryPending, retryTrigger). */
|
|
291
|
+
resetState() {
|
|
292
|
+
this.activeHook = null;
|
|
293
|
+
this.hookQueue = [];
|
|
294
|
+
this.cycleCounts.clear();
|
|
295
|
+
this.retryPending = false;
|
|
296
|
+
this.retryTrigger = null;
|
|
297
|
+
}
|
|
298
|
+
// ── Persistence ─────────────────────────────────────────────────────
|
|
299
|
+
_hookStatePath(basePath) {
|
|
300
|
+
return join(basePath, ".gsd", HOOK_STATE_FILE);
|
|
301
|
+
}
|
|
302
|
+
/** Persist current hook cycle counts to disk. */
|
|
303
|
+
persistState(basePath) {
|
|
304
|
+
const state = {
|
|
305
|
+
cycleCounts: Object.fromEntries(this.cycleCounts),
|
|
306
|
+
savedAt: new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
try {
|
|
309
|
+
const dir = join(basePath, ".gsd");
|
|
310
|
+
if (!existsSync(dir))
|
|
311
|
+
mkdirSync(dir, { recursive: true });
|
|
312
|
+
writeFileSync(this._hookStatePath(basePath), JSON.stringify(state, null, 2), "utf-8");
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Non-fatal — state is recreatable from artifacts
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Restore hook cycle counts from disk after a crash/restart. */
|
|
319
|
+
restoreState(basePath) {
|
|
320
|
+
try {
|
|
321
|
+
const filePath = this._hookStatePath(basePath);
|
|
322
|
+
if (!existsSync(filePath))
|
|
323
|
+
return;
|
|
324
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
325
|
+
const state = JSON.parse(raw);
|
|
326
|
+
if (state.cycleCounts && typeof state.cycleCounts === "object") {
|
|
327
|
+
this.cycleCounts.clear();
|
|
328
|
+
for (const [key, value] of Object.entries(state.cycleCounts)) {
|
|
329
|
+
if (typeof value === "number") {
|
|
330
|
+
this.cycleCounts.set(key, value);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Non-fatal — fresh state is fine
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Clear persisted hook state file from disk. */
|
|
340
|
+
clearPersistedState(basePath) {
|
|
341
|
+
try {
|
|
342
|
+
const filePath = this._hookStatePath(basePath);
|
|
343
|
+
if (existsSync(filePath)) {
|
|
344
|
+
writeFileSync(filePath, JSON.stringify({ cycleCounts: {}, savedAt: new Date().toISOString() }, null, 2), "utf-8");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Non-fatal
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Hook status reporting ───────────────────────────────────────────
|
|
352
|
+
/** Get status of all configured hooks for display. */
|
|
353
|
+
getHookStatus() {
|
|
354
|
+
const entries = [];
|
|
355
|
+
const postHooks = resolvePostUnitHooks();
|
|
356
|
+
for (const hook of postHooks) {
|
|
357
|
+
const activeCycles = {};
|
|
358
|
+
for (const [key, count] of this.cycleCounts) {
|
|
359
|
+
if (key.startsWith(`${hook.name}/`)) {
|
|
360
|
+
activeCycles[key] = count;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
entries.push({
|
|
364
|
+
name: hook.name,
|
|
365
|
+
type: "post",
|
|
366
|
+
enabled: hook.enabled !== false,
|
|
367
|
+
targets: hook.after,
|
|
368
|
+
activeCycles,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const preHooks = resolvePreDispatchHooks();
|
|
372
|
+
for (const hook of preHooks) {
|
|
373
|
+
entries.push({
|
|
374
|
+
name: hook.name,
|
|
375
|
+
type: "pre",
|
|
376
|
+
enabled: hook.enabled !== false,
|
|
377
|
+
targets: hook.before,
|
|
378
|
+
activeCycles: {},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return entries;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Manually trigger a specific hook for a unit.
|
|
385
|
+
* Bypasses normal flow — forces hook to run even if artifact exists.
|
|
386
|
+
*/
|
|
387
|
+
triggerHookManually(hookName, unitType, unitId, basePath) {
|
|
388
|
+
const hook = resolvePostUnitHooks().find(h => h.name === hookName);
|
|
389
|
+
if (!hook) {
|
|
390
|
+
console.error(`[triggerHookManually] Hook "${hookName}" not found in post_unit_hooks`);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (!hook.prompt || typeof hook.prompt !== "string" || hook.prompt.trim().length === 0) {
|
|
394
|
+
console.error(`[triggerHookManually] Hook "${hookName}" has empty prompt`);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
this.activeHook = {
|
|
398
|
+
hookName: hook.name,
|
|
399
|
+
triggerUnitType: unitType,
|
|
400
|
+
triggerUnitId: unitId,
|
|
401
|
+
cycle: 1,
|
|
402
|
+
pendingRetry: false,
|
|
403
|
+
};
|
|
404
|
+
this.hookQueue = [{
|
|
405
|
+
config: hook,
|
|
406
|
+
triggerUnitType: unitType,
|
|
407
|
+
triggerUnitId: unitId,
|
|
408
|
+
}];
|
|
409
|
+
const cycleKey = `${hook.name}/${unitType}/${unitId}`;
|
|
410
|
+
const currentCycle = (this.cycleCounts.get(cycleKey) ?? 0) + 1;
|
|
411
|
+
this.cycleCounts.set(cycleKey, currentCycle);
|
|
412
|
+
this.activeHook.cycle = currentCycle;
|
|
413
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
414
|
+
const prompt = hook.prompt
|
|
415
|
+
.replace(/\{milestoneId\}/g, mid ?? "")
|
|
416
|
+
.replace(/\{sliceId\}/g, sid ?? "")
|
|
417
|
+
.replace(/\{taskId\}/g, tid ?? "");
|
|
418
|
+
return {
|
|
419
|
+
hookName: hook.name,
|
|
420
|
+
prompt,
|
|
421
|
+
model: hook.model,
|
|
422
|
+
unitType: `hook/${hook.name}`,
|
|
423
|
+
unitId,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** Format hook status for terminal display. */
|
|
427
|
+
formatHookStatus() {
|
|
428
|
+
const entries = this.getHookStatus();
|
|
429
|
+
if (entries.length === 0) {
|
|
430
|
+
return "No hooks configured. Add post_unit_hooks or pre_dispatch_hooks to .gsd/preferences.md";
|
|
431
|
+
}
|
|
432
|
+
const lines = ["Configured Hooks:", ""];
|
|
433
|
+
const postHooks = entries.filter(e => e.type === "post");
|
|
434
|
+
const preHooks = entries.filter(e => e.type === "pre");
|
|
435
|
+
if (postHooks.length > 0) {
|
|
436
|
+
lines.push("Post-Unit Hooks (run after unit completes):");
|
|
437
|
+
for (const hook of postHooks) {
|
|
438
|
+
const status = hook.enabled ? "enabled" : "disabled";
|
|
439
|
+
const cycles = Object.keys(hook.activeCycles).length;
|
|
440
|
+
const cycleInfo = cycles > 0 ? ` (${cycles} active cycle${cycles === 1 ? "" : "s"})` : "";
|
|
441
|
+
lines.push(` ${hook.name} [${status}] → after: ${hook.targets.join(", ")}${cycleInfo}`);
|
|
442
|
+
}
|
|
443
|
+
lines.push("");
|
|
444
|
+
}
|
|
445
|
+
if (preHooks.length > 0) {
|
|
446
|
+
lines.push("Pre-Dispatch Hooks (run before unit dispatches):");
|
|
447
|
+
for (const hook of preHooks) {
|
|
448
|
+
const status = hook.enabled ? "enabled" : "disabled";
|
|
449
|
+
lines.push(` ${hook.name} [${status}] → before: ${hook.targets.join(", ")}`);
|
|
450
|
+
}
|
|
451
|
+
lines.push("");
|
|
452
|
+
}
|
|
453
|
+
return lines.join("\n");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ─── Module-level Singleton ─────────────────────────────────────────────────
|
|
457
|
+
let _registry = null;
|
|
458
|
+
/** Get the singleton registry. Throws if not initialized. */
|
|
459
|
+
export function getRegistry() {
|
|
460
|
+
if (!_registry) {
|
|
461
|
+
throw new Error("RuleRegistry not initialized — call initRegistry() or setRegistry() first.");
|
|
462
|
+
}
|
|
463
|
+
return _registry;
|
|
464
|
+
}
|
|
465
|
+
/** Set the singleton registry instance. */
|
|
466
|
+
export function setRegistry(r) {
|
|
467
|
+
_registry = r;
|
|
468
|
+
}
|
|
469
|
+
/** Create and set the singleton registry with the given dispatch rules. */
|
|
470
|
+
export function initRegistry(dispatchRules) {
|
|
471
|
+
const registry = new RuleRegistry(dispatchRules);
|
|
472
|
+
setRegistry(registry);
|
|
473
|
+
return registry;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get the singleton registry, lazily creating one with empty dispatch rules
|
|
477
|
+
* if not yet initialized. This ensures facade functions work even when
|
|
478
|
+
* the full registry hasn't been set up (e.g. during testing).
|
|
479
|
+
*/
|
|
480
|
+
export function getOrCreateRegistry() {
|
|
481
|
+
if (!_registry) {
|
|
482
|
+
_registry = new RuleRegistry([]);
|
|
483
|
+
}
|
|
484
|
+
return _registry;
|
|
485
|
+
}
|
|
486
|
+
/** Reset the singleton (for testing). */
|
|
487
|
+
export function resetRegistry() {
|
|
488
|
+
_registry = null;
|
|
489
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// GSD Extension — Unified Rule Type Definitions
|
|
2
|
+
//
|
|
3
|
+
// Every dispatch rule and hook is expressed as a `UnifiedRule` with a
|
|
4
|
+
// consistent when/where/then shape. This file defines the type system;
|
|
5
|
+
// the `RuleRegistry` class in rule-registry.ts holds instances at runtime.
|
|
6
|
+
export {};
|