@hegemonart/get-design-done 1.30.6 → 1.31.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +6 -3
- package/.claude-plugin/plugin.json +5 -2
- package/CHANGELOG.md +105 -0
- package/NOTICE +224 -0
- package/README.md +22 -1
- package/SKILL.md +1 -0
- package/agents/design-authority-watcher.md +1 -1
- package/agents/perf-analyzer.md +2 -2
- package/bin/gdd-mcp +78 -0
- package/bin/gdd-sdk +34 -24
- package/bin/gdd-state-mcp +78 -0
- package/{README.de.md → docs/i18n/README.de.md} +1 -1
- package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
- package/{README.it.md → docs/i18n/README.it.md} +1 -1
- package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
- package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
- package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
- package/hooks/_hook-emit.js +1 -1
- package/hooks/budget-enforcer.ts +5 -5
- package/hooks/context-exhaustion.ts +2 -2
- package/hooks/gdd-precompact-snapshot.js +3 -3
- package/hooks/gdd-read-injection-scanner.ts +2 -2
- package/hooks/gdd-sessionstart-recap.js +1 -1
- package/hooks/gdd-turn-closeout.js +1 -1
- package/package.json +24 -10
- package/recipes/.gitkeep +0 -0
- package/reference/schemas/recipe.schema.json +33 -0
- package/scripts/cli/gdd-events.mjs +5 -5
- package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
- package/scripts/lib/cli/index.ts +22 -160
- package/scripts/lib/connection-probe/index.cjs +1 -1
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
- package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
- package/scripts/lib/error-classifier.cjs +24 -227
- package/scripts/lib/event-stream/index.ts +25 -193
- package/scripts/lib/figma-extract/digest.cjs +430 -0
- package/scripts/lib/figma-extract/parse-url.cjs +87 -0
- package/scripts/lib/figma-extract/payload-schema.json +108 -0
- package/scripts/lib/figma-extract/pull.cjs +394 -0
- package/scripts/lib/figma-extract/receiver.cjs +273 -0
- package/scripts/lib/figma-extract/render-md.cjs +143 -0
- package/scripts/lib/figma-extract/styles-resolver.cjs +147 -0
- package/scripts/lib/figma-extract/walk.cjs +100 -0
- package/scripts/lib/gdd-errors/index.ts +24 -213
- package/scripts/lib/gdd-state/index.ts +23 -161
- package/scripts/lib/health-mirror/index.cjs +88 -1
- package/scripts/lib/iteration-budget.cjs +23 -199
- package/scripts/lib/jittered-backoff.cjs +24 -107
- package/scripts/lib/lockfile.cjs +23 -195
- package/scripts/lib/logger/index.ts +1 -1
- package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
- package/scripts/lib/perf-analyzer/index.cjs +1 -1
- package/scripts/lib/pipeline-runner/index.ts +4 -4
- package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
- package/scripts/lib/prompt-dedup/index.cjs +1 -1
- package/scripts/lib/rate-guard.cjs +2 -2
- package/scripts/lib/recipe-loader.cjs +142 -0
- package/scripts/lib/session-runner/errors.ts +3 -3
- package/scripts/lib/session-runner/index.ts +3 -3
- package/scripts/lib/session-runner/transcript.ts +1 -1
- package/scripts/lib/tool-scoping/index.ts +1 -1
- package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
- package/scripts/mcp-servers/gdd-state/server.ts +28 -282
- package/sdk/README.md +45 -0
- package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
- package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
- package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
- package/sdk/cli/index.js +8091 -0
- package/sdk/cli/index.ts +172 -0
- package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
- package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
- package/sdk/errors/index.ts +218 -0
- package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
- package/sdk/event-stream/index.ts +197 -0
- package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
- package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
- package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
- package/sdk/index.ts +19 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
- package/sdk/mcp/gdd-mcp/server.js +1924 -0
- package/sdk/mcp/gdd-mcp/server.ts +325 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
- package/sdk/mcp/gdd-state/server.js +2790 -0
- package/sdk/mcp/gdd-state/server.ts +294 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
- package/sdk/primitives/error-classifier.cjs +232 -0
- package/sdk/primitives/iteration-budget.cjs +205 -0
- package/sdk/primitives/jittered-backoff.cjs +112 -0
- package/sdk/primitives/lockfile.cjs +201 -0
- package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
- package/sdk/state/index.ts +167 -0
- package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
- package/skills/figma-extract/SKILL.md +64 -0
- package/skills/health/SKILL.md +10 -0
- package/skills/quality-gate/SKILL.md +2 -2
- package/scripts/aggregate-agent-metrics.ts +0 -282
- package/scripts/bootstrap-manifest.txt +0 -3
- package/scripts/bootstrap.sh +0 -80
- package/scripts/build-distribution-bundles.cjs +0 -549
- package/scripts/build-intel.cjs +0 -486
- package/scripts/codegen-schema-types.ts +0 -149
- package/scripts/detect-stale-refs.cjs +0 -107
- package/scripts/e2e/run-headless.ts +0 -514
- package/scripts/extract-changelog-section.cjs +0 -58
- package/scripts/gsd-cleanup-incubator.cjs +0 -367
- package/scripts/injection-patterns.cjs +0 -58
- package/scripts/lint-agentskills-spec.cjs +0 -457
- package/scripts/release-smoke-test.cjs +0 -200
- package/scripts/rollback-release.sh +0 -42
- package/scripts/run-injection-scanner-ci.cjs +0 -83
- package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
- package/scripts/tests/test-authority-watcher-diff.sh +0 -113
- package/scripts/tests/test-motion-provenance.sh +0 -64
- package/scripts/validate-frontmatter.ts +0 -409
- package/scripts/validate-incubator-scope.cjs +0 -133
- package/scripts/validate-schemas.ts +0 -401
- package/scripts/validate-skill-length.cjs +0 -283
- package/scripts/verify-version-sync.cjs +0 -30
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
- /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
|
@@ -0,0 +1,2790 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name12 in all)
|
|
9
|
+
__defProp(target, name12, { get: all[name12], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// sdk/mcp/gdd-state/server.ts
|
|
22
|
+
var server_exports = {};
|
|
23
|
+
__export(server_exports, {
|
|
24
|
+
buildServer: () => buildServer,
|
|
25
|
+
runStdio: () => runStdio
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(server_exports);
|
|
28
|
+
var import_node_fs4 = require("node:fs");
|
|
29
|
+
var import_node_path2 = require("node:path");
|
|
30
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
31
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
32
|
+
var import_types8 = require("@modelcontextprotocol/sdk/types.js");
|
|
33
|
+
|
|
34
|
+
// sdk/errors/index.ts
|
|
35
|
+
var GDDError = class extends Error {
|
|
36
|
+
code;
|
|
37
|
+
context;
|
|
38
|
+
constructor(message, code, context = {}) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.context = Object.freeze({ ...context });
|
|
42
|
+
this.name = new.target.name;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Serialize to a plain object safe for JSON.stringify. Round-trips
|
|
46
|
+
* through JSON without loss of `name`, `kind`, `code`, `message`, or
|
|
47
|
+
* `context`. Does NOT include the stack trace — MCP tool handlers do
|
|
48
|
+
* not forward stacks to the model.
|
|
49
|
+
*/
|
|
50
|
+
toJSON() {
|
|
51
|
+
return {
|
|
52
|
+
name: this.name,
|
|
53
|
+
kind: this.kind,
|
|
54
|
+
code: this.code,
|
|
55
|
+
message: this.message,
|
|
56
|
+
context: this.context
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var ValidationError = class extends GDDError {
|
|
61
|
+
kind = "validation";
|
|
62
|
+
constructor(message, code = "VALIDATION", context) {
|
|
63
|
+
super(message, code, context);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var StateConflictError = class extends GDDError {
|
|
67
|
+
kind = "state_conflict";
|
|
68
|
+
constructor(message, code = "STATE_CONFLICT", context) {
|
|
69
|
+
super(message, code, context);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var OperationFailedError = class extends GDDError {
|
|
73
|
+
kind = "operation_failed";
|
|
74
|
+
constructor(message, code = "OPERATION_FAILED", context) {
|
|
75
|
+
super(message, code, context);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var LockAcquisitionError = class extends StateConflictError {
|
|
79
|
+
lockPath;
|
|
80
|
+
lockContents;
|
|
81
|
+
waitedMs;
|
|
82
|
+
constructor(lockPath, lockContents, waitedMs, context) {
|
|
83
|
+
super(
|
|
84
|
+
`failed to acquire lock at ${lockPath} after ${waitedMs}ms; current holder: ${lockContents}`,
|
|
85
|
+
"LOCK_ACQUISITION",
|
|
86
|
+
{ ...context, lockPath, lockContents, waitedMs }
|
|
87
|
+
);
|
|
88
|
+
this.lockPath = lockPath;
|
|
89
|
+
this.lockContents = lockContents;
|
|
90
|
+
this.waitedMs = waitedMs;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var TransitionGateFailed = class extends StateConflictError {
|
|
94
|
+
blockers;
|
|
95
|
+
toStage;
|
|
96
|
+
constructor(toStage, blockers, context) {
|
|
97
|
+
super(
|
|
98
|
+
`transition to "${toStage}" blocked by gate: ${blockers.join("; ") || "(no detail)"}`,
|
|
99
|
+
"TRANSITION_GATE_FAILED",
|
|
100
|
+
{ ...context, toStage, blockers: [...blockers] }
|
|
101
|
+
);
|
|
102
|
+
this.toStage = toStage;
|
|
103
|
+
this.blockers = Object.freeze([...blockers]);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var ParseError = class extends ValidationError {
|
|
107
|
+
line;
|
|
108
|
+
constructor(message, line, context) {
|
|
109
|
+
super(
|
|
110
|
+
`STATE.md parse error at line ${line}: ${message}`,
|
|
111
|
+
"PARSE_ERROR",
|
|
112
|
+
{ ...context, line }
|
|
113
|
+
);
|
|
114
|
+
this.line = line;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// sdk/errors/classification.ts
|
|
119
|
+
function classify(err) {
|
|
120
|
+
if (err instanceof ValidationError) {
|
|
121
|
+
return {
|
|
122
|
+
kind: "validation",
|
|
123
|
+
shouldThrow: true,
|
|
124
|
+
retryable: false,
|
|
125
|
+
code: err.code,
|
|
126
|
+
message: err.message
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (err instanceof StateConflictError) {
|
|
130
|
+
return {
|
|
131
|
+
kind: "state_conflict",
|
|
132
|
+
shouldThrow: true,
|
|
133
|
+
retryable: true,
|
|
134
|
+
code: err.code,
|
|
135
|
+
message: err.message
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (err instanceof OperationFailedError) {
|
|
139
|
+
return {
|
|
140
|
+
kind: "operation_failed",
|
|
141
|
+
shouldThrow: false,
|
|
142
|
+
retryable: false,
|
|
143
|
+
code: err.code,
|
|
144
|
+
message: err.message
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (err instanceof Error) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "unknown",
|
|
150
|
+
shouldThrow: true,
|
|
151
|
+
retryable: false,
|
|
152
|
+
code: "UNKNOWN",
|
|
153
|
+
message: err.message
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
kind: "unknown",
|
|
158
|
+
shouldThrow: true,
|
|
159
|
+
retryable: false,
|
|
160
|
+
code: "UNKNOWN",
|
|
161
|
+
message: String(err)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function toToolError(err) {
|
|
165
|
+
const c = classify(err);
|
|
166
|
+
const out = {
|
|
167
|
+
error: {
|
|
168
|
+
code: c.code,
|
|
169
|
+
message: c.message,
|
|
170
|
+
kind: c.kind
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
if (err instanceof GDDError) {
|
|
174
|
+
out.error.context = err.context;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// sdk/mcp/gdd-state/tools/get.ts
|
|
180
|
+
var get_exports = {};
|
|
181
|
+
__export(get_exports, {
|
|
182
|
+
handle: () => handle,
|
|
183
|
+
name: () => name,
|
|
184
|
+
schemaPath: () => schemaPath
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// sdk/state/index.ts
|
|
188
|
+
var import_node_fs2 = require("node:fs");
|
|
189
|
+
|
|
190
|
+
// sdk/state/lockfile.ts
|
|
191
|
+
var import_node_fs = require("node:fs");
|
|
192
|
+
var import_node_os = require("node:os");
|
|
193
|
+
|
|
194
|
+
// sdk/state/types.ts
|
|
195
|
+
function isStage(value) {
|
|
196
|
+
return value === "brief" || value === "explore" || value === "plan" || value === "design" || value === "verify";
|
|
197
|
+
}
|
|
198
|
+
function isConnectionStatus(value) {
|
|
199
|
+
return value === "available" || value === "unavailable" || value === "not_configured";
|
|
200
|
+
}
|
|
201
|
+
function isDecisionStatus(value) {
|
|
202
|
+
return value === "locked" || value === "tentative";
|
|
203
|
+
}
|
|
204
|
+
function isMustHaveStatus(value) {
|
|
205
|
+
return value === "pending" || value === "pass" || value === "fail";
|
|
206
|
+
}
|
|
207
|
+
function isSpikeVerdict(value) {
|
|
208
|
+
return value === "yes" || value === "no" || value === "partial";
|
|
209
|
+
}
|
|
210
|
+
function isPrototypingEntryStatus(value) {
|
|
211
|
+
return value === "resolved";
|
|
212
|
+
}
|
|
213
|
+
function isQualityGateStatus(value) {
|
|
214
|
+
return value === "pass" || value === "fail" || value === "timeout" || value === "skipped";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// sdk/state/lockfile.ts
|
|
218
|
+
var DEFAULTS = {
|
|
219
|
+
staleMs: 6e4,
|
|
220
|
+
maxWaitMs: 5e3,
|
|
221
|
+
pollMs: 50
|
|
222
|
+
};
|
|
223
|
+
async function acquire(path, opts = {}) {
|
|
224
|
+
const staleMs = opts.staleMs ?? DEFAULTS.staleMs;
|
|
225
|
+
const maxWaitMs = opts.maxWaitMs ?? DEFAULTS.maxWaitMs;
|
|
226
|
+
const pollMs = opts.pollMs ?? DEFAULTS.pollMs;
|
|
227
|
+
const lockPath = `${path}.lock`;
|
|
228
|
+
const startedAt = Date.now();
|
|
229
|
+
const payload = {
|
|
230
|
+
pid: process.pid,
|
|
231
|
+
host: (0, import_node_os.hostname)(),
|
|
232
|
+
acquired_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
233
|
+
};
|
|
234
|
+
const payloadText = JSON.stringify(payload);
|
|
235
|
+
if (maxWaitMs < 0 || pollMs < 0 || staleMs < 0) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`invalid AcquireOptions: staleMs=${staleMs}, maxWaitMs=${maxWaitMs}, pollMs=${pollMs}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
while (true) {
|
|
241
|
+
try {
|
|
242
|
+
(0, import_node_fs.writeFileSync)(lockPath, payloadText, { flag: "wx", encoding: "utf8" });
|
|
243
|
+
return makeRelease(lockPath);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const code = getErrnoCode(err);
|
|
246
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "EBUSY") {
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
const existing = readLockSafe(lockPath);
|
|
250
|
+
if (existing === null) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const parsed = parseLock(existing);
|
|
254
|
+
if (parsed !== null && isStale(parsed, staleMs)) {
|
|
255
|
+
try {
|
|
256
|
+
(0, import_node_fs.unlinkSync)(lockPath);
|
|
257
|
+
} catch (delErr) {
|
|
258
|
+
const delCode = getErrnoCode(delErr);
|
|
259
|
+
if (delCode !== "ENOENT") {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
265
|
+
throw new LockAcquisitionError(lockPath, existing, Date.now() - startedAt);
|
|
266
|
+
}
|
|
267
|
+
await sleep(pollMs);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function makeRelease(lockPath) {
|
|
272
|
+
let released = false;
|
|
273
|
+
return async () => {
|
|
274
|
+
if (released) return;
|
|
275
|
+
released = true;
|
|
276
|
+
try {
|
|
277
|
+
(0, import_node_fs.unlinkSync)(lockPath);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const code = getErrnoCode(err);
|
|
280
|
+
if (code !== "ENOENT") {
|
|
281
|
+
if (code === "EPERM" || code === "EBUSY") {
|
|
282
|
+
await sleep(50);
|
|
283
|
+
try {
|
|
284
|
+
if ((0, import_node_fs.existsSync)(lockPath)) (0, import_node_fs.unlinkSync)(lockPath);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function readLockSafe(path) {
|
|
293
|
+
try {
|
|
294
|
+
return (0, import_node_fs.readFileSync)(path, "utf8");
|
|
295
|
+
} catch (err) {
|
|
296
|
+
const code = getErrnoCode(err);
|
|
297
|
+
if (code === "ENOENT") return null;
|
|
298
|
+
return "<unreadable>";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function parseLock(raw) {
|
|
302
|
+
try {
|
|
303
|
+
const obj = JSON.parse(raw);
|
|
304
|
+
if (typeof obj === "object" && obj !== null && typeof obj["pid"] === "number" && typeof obj["host"] === "string" && typeof obj["acquired_at"] === "string") {
|
|
305
|
+
return obj;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function isStale(payload, staleMs) {
|
|
313
|
+
if (!isPidAlive(payload.pid, payload.host)) return true;
|
|
314
|
+
const acquiredAt = Date.parse(payload.acquired_at);
|
|
315
|
+
if (!Number.isFinite(acquiredAt)) return true;
|
|
316
|
+
return Date.now() - acquiredAt > staleMs;
|
|
317
|
+
}
|
|
318
|
+
function isPidAlive(pid, host) {
|
|
319
|
+
if (host !== (0, import_node_os.hostname)()) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
if (pid === process.pid) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
process.kill(pid, 0);
|
|
327
|
+
return true;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
const code = getErrnoCode(err);
|
|
330
|
+
if (code === "ESRCH") return false;
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function getErrnoCode(err) {
|
|
335
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
336
|
+
const code = err.code;
|
|
337
|
+
if (typeof code === "string") return code;
|
|
338
|
+
}
|
|
339
|
+
return void 0;
|
|
340
|
+
}
|
|
341
|
+
function sleep(ms) {
|
|
342
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// sdk/state/parser.ts
|
|
346
|
+
var BLOCK_ORDER = [
|
|
347
|
+
"position",
|
|
348
|
+
"decisions",
|
|
349
|
+
"must_haves",
|
|
350
|
+
"prototyping",
|
|
351
|
+
"quality_gate",
|
|
352
|
+
"connections",
|
|
353
|
+
"blockers",
|
|
354
|
+
"parallelism_decision",
|
|
355
|
+
"todos",
|
|
356
|
+
"timestamps"
|
|
357
|
+
];
|
|
358
|
+
var EMPTY_RAW_BODIES = {
|
|
359
|
+
position: null,
|
|
360
|
+
decisions: null,
|
|
361
|
+
must_haves: null,
|
|
362
|
+
prototyping: null,
|
|
363
|
+
quality_gate: null,
|
|
364
|
+
connections: null,
|
|
365
|
+
blockers: null,
|
|
366
|
+
parallelism_decision: null,
|
|
367
|
+
todos: null,
|
|
368
|
+
timestamps: null
|
|
369
|
+
};
|
|
370
|
+
function parse(raw) {
|
|
371
|
+
const line_ending = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
372
|
+
const normalized = line_ending === "\r\n" ? raw.replace(/\r\n/g, "\n") : raw;
|
|
373
|
+
const trailing_newline = normalized.endsWith("\n");
|
|
374
|
+
if (!normalized.startsWith("---\n")) {
|
|
375
|
+
throw new ParseError('file must begin with "---" frontmatter fence', 1);
|
|
376
|
+
}
|
|
377
|
+
const fmEnd = normalized.indexOf("\n---\n", 4);
|
|
378
|
+
if (fmEnd === -1) {
|
|
379
|
+
throw new ParseError('unterminated frontmatter (missing closing "---")', 1);
|
|
380
|
+
}
|
|
381
|
+
const fmText = normalized.slice(4, fmEnd);
|
|
382
|
+
const frontmatter = parseFrontmatter(fmText);
|
|
383
|
+
const afterFm = fmEnd + 5;
|
|
384
|
+
const body = normalized.slice(afterFm);
|
|
385
|
+
const lines = body.split("\n");
|
|
386
|
+
const blocks = [];
|
|
387
|
+
const seen = /* @__PURE__ */ new Set();
|
|
388
|
+
const blockOpen = /^<([a-z_]+)>\s*$/;
|
|
389
|
+
const blockClose = /^<\/([a-z_]+)>\s*$/;
|
|
390
|
+
for (let i = 0; i < lines.length; i++) {
|
|
391
|
+
const line = lines[i] ?? "";
|
|
392
|
+
const openMatch = line.match(blockOpen);
|
|
393
|
+
if (!openMatch) continue;
|
|
394
|
+
const name12 = openMatch[1];
|
|
395
|
+
if (!BLOCK_ORDER.includes(name12)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (seen.has(name12)) {
|
|
399
|
+
throw new ParseError(`duplicate block <${name12}>`, lineToFileLine(afterFm, normalized, i));
|
|
400
|
+
}
|
|
401
|
+
let close = -1;
|
|
402
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
403
|
+
const cm = (lines[j] ?? "").match(blockClose);
|
|
404
|
+
if (cm && cm[1] === name12) {
|
|
405
|
+
close = j;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (close === -1) {
|
|
410
|
+
throw new ParseError(
|
|
411
|
+
`unterminated block <${name12}> (no </${name12}>)`,
|
|
412
|
+
lineToFileLine(afterFm, normalized, i)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
blocks.push({ name: name12, openLine: i, closeLine: close });
|
|
416
|
+
seen.add(name12);
|
|
417
|
+
i = close;
|
|
418
|
+
}
|
|
419
|
+
let body_preamble;
|
|
420
|
+
let body_trailer;
|
|
421
|
+
const block_gaps = {
|
|
422
|
+
position: "",
|
|
423
|
+
decisions: "",
|
|
424
|
+
must_haves: "",
|
|
425
|
+
prototyping: "",
|
|
426
|
+
quality_gate: "",
|
|
427
|
+
connections: "",
|
|
428
|
+
blockers: "",
|
|
429
|
+
parallelism_decision: "",
|
|
430
|
+
todos: "",
|
|
431
|
+
timestamps: ""
|
|
432
|
+
};
|
|
433
|
+
if (blocks.length === 0) {
|
|
434
|
+
body_preamble = body;
|
|
435
|
+
body_trailer = "";
|
|
436
|
+
} else {
|
|
437
|
+
const firstBlock = blocks[0];
|
|
438
|
+
const lastBlock = blocks[blocks.length - 1];
|
|
439
|
+
if (firstBlock === void 0 || lastBlock === void 0) {
|
|
440
|
+
throw new ParseError("internal: block index inconsistency", 1);
|
|
441
|
+
}
|
|
442
|
+
body_preamble = lines.slice(0, firstBlock.openLine).join("\n");
|
|
443
|
+
if (firstBlock.openLine > 0) body_preamble += "\n";
|
|
444
|
+
body_trailer = lines.slice(lastBlock.closeLine + 1).join("\n");
|
|
445
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
446
|
+
const cur = blocks[bi];
|
|
447
|
+
if (cur === void 0) continue;
|
|
448
|
+
if (bi === 0) {
|
|
449
|
+
block_gaps[cur.name] = body_preamble;
|
|
450
|
+
} else {
|
|
451
|
+
const prev = blocks[bi - 1];
|
|
452
|
+
if (prev === void 0) continue;
|
|
453
|
+
const gapLines = lines.slice(prev.closeLine + 1, cur.openLine);
|
|
454
|
+
let gap = gapLines.join("\n");
|
|
455
|
+
if (cur.openLine > prev.closeLine + 1) gap += "\n";
|
|
456
|
+
block_gaps[cur.name] = gap;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const raw_bodies = { ...EMPTY_RAW_BODIES };
|
|
461
|
+
let position = null;
|
|
462
|
+
let decisions = [];
|
|
463
|
+
let must_haves = [];
|
|
464
|
+
let connections = {};
|
|
465
|
+
let blockers = [];
|
|
466
|
+
let parallelism_decision = null;
|
|
467
|
+
let todos = null;
|
|
468
|
+
let prototyping = null;
|
|
469
|
+
let quality_gate = null;
|
|
470
|
+
let timestamps = {};
|
|
471
|
+
for (const blk of blocks) {
|
|
472
|
+
const rawBody = lines.slice(blk.openLine + 1, blk.closeLine).join("\n");
|
|
473
|
+
raw_bodies[blk.name] = rawBody;
|
|
474
|
+
const fileLineOfBody = lineToFileLine(afterFm, normalized, blk.openLine + 1);
|
|
475
|
+
switch (blk.name) {
|
|
476
|
+
case "position":
|
|
477
|
+
position = parsePositionBody(rawBody, fileLineOfBody);
|
|
478
|
+
break;
|
|
479
|
+
case "decisions":
|
|
480
|
+
decisions = parseDecisionsBody(rawBody, fileLineOfBody);
|
|
481
|
+
break;
|
|
482
|
+
case "must_haves":
|
|
483
|
+
must_haves = parseMustHavesBody(rawBody, fileLineOfBody);
|
|
484
|
+
break;
|
|
485
|
+
case "prototyping":
|
|
486
|
+
prototyping = parsePrototypingBody(rawBody, fileLineOfBody);
|
|
487
|
+
break;
|
|
488
|
+
case "quality_gate":
|
|
489
|
+
quality_gate = parseQualityGateBody(rawBody, fileLineOfBody);
|
|
490
|
+
break;
|
|
491
|
+
case "connections":
|
|
492
|
+
connections = parseConnectionsBody(rawBody, fileLineOfBody);
|
|
493
|
+
break;
|
|
494
|
+
case "blockers":
|
|
495
|
+
blockers = parseBlockersBody(rawBody, fileLineOfBody);
|
|
496
|
+
break;
|
|
497
|
+
case "parallelism_decision":
|
|
498
|
+
parallelism_decision = rawBody;
|
|
499
|
+
break;
|
|
500
|
+
case "todos":
|
|
501
|
+
todos = rawBody;
|
|
502
|
+
break;
|
|
503
|
+
case "timestamps":
|
|
504
|
+
timestamps = parseTimestampsBody(rawBody, fileLineOfBody);
|
|
505
|
+
break;
|
|
506
|
+
default: {
|
|
507
|
+
const _exhaustive = blk.name;
|
|
508
|
+
void _exhaustive;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (position === null) {
|
|
513
|
+
throw new ParseError(
|
|
514
|
+
"missing required <position> block (run scan to initialize STATE.md)",
|
|
515
|
+
1
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const state = {
|
|
519
|
+
frontmatter,
|
|
520
|
+
position,
|
|
521
|
+
decisions,
|
|
522
|
+
must_haves,
|
|
523
|
+
connections,
|
|
524
|
+
blockers,
|
|
525
|
+
parallelism_decision,
|
|
526
|
+
todos,
|
|
527
|
+
prototyping,
|
|
528
|
+
quality_gate,
|
|
529
|
+
timestamps,
|
|
530
|
+
body_preamble,
|
|
531
|
+
body_trailer
|
|
532
|
+
};
|
|
533
|
+
return {
|
|
534
|
+
state,
|
|
535
|
+
raw_bodies,
|
|
536
|
+
raw_frontmatter: fmText,
|
|
537
|
+
block_gaps,
|
|
538
|
+
line_ending,
|
|
539
|
+
trailing_newline
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function lineToFileLine(bodyStartOffset, normalized, bodyLineIdx) {
|
|
543
|
+
const prefix = normalized.slice(0, bodyStartOffset);
|
|
544
|
+
const prefixLines = prefix.split("\n").length - 1;
|
|
545
|
+
return prefixLines + bodyLineIdx + 1;
|
|
546
|
+
}
|
|
547
|
+
function parseFrontmatter(raw) {
|
|
548
|
+
const out = {};
|
|
549
|
+
const lines = raw.split("\n");
|
|
550
|
+
for (const line of lines) {
|
|
551
|
+
const trimmed = line.trim();
|
|
552
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
553
|
+
const idx = line.indexOf(":");
|
|
554
|
+
if (idx === -1) continue;
|
|
555
|
+
const key = line.slice(0, idx).trim();
|
|
556
|
+
let value = line.slice(idx + 1).trim();
|
|
557
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
558
|
+
value = value.slice(1, -1);
|
|
559
|
+
}
|
|
560
|
+
if (key === "wave") {
|
|
561
|
+
const n = Number(value);
|
|
562
|
+
out[key] = Number.isFinite(n) ? n : value;
|
|
563
|
+
} else {
|
|
564
|
+
out[key] = value;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const fm = {
|
|
568
|
+
pipeline_state_version: String(out["pipeline_state_version"] ?? "1.0"),
|
|
569
|
+
stage: String(out["stage"] ?? ""),
|
|
570
|
+
cycle: String(out["cycle"] ?? ""),
|
|
571
|
+
wave: typeof out["wave"] === "number" ? out["wave"] : 1,
|
|
572
|
+
started_at: String(out["started_at"] ?? ""),
|
|
573
|
+
last_checkpoint: String(out["last_checkpoint"] ?? "")
|
|
574
|
+
};
|
|
575
|
+
for (const [k, v] of Object.entries(out)) {
|
|
576
|
+
if (!(k in fm)) fm[k] = v;
|
|
577
|
+
}
|
|
578
|
+
return fm;
|
|
579
|
+
}
|
|
580
|
+
function parsePositionBody(body, startLine) {
|
|
581
|
+
const fields = {};
|
|
582
|
+
const lines = body.split("\n");
|
|
583
|
+
for (let i = 0; i < lines.length; i++) {
|
|
584
|
+
const line = lines[i] ?? "";
|
|
585
|
+
const trimmed = line.trim();
|
|
586
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
587
|
+
const idx = line.indexOf(":");
|
|
588
|
+
if (idx === -1) continue;
|
|
589
|
+
const key = line.slice(0, idx).trim();
|
|
590
|
+
let value = line.slice(idx + 1).trim();
|
|
591
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
592
|
+
value = value.slice(1, -1);
|
|
593
|
+
}
|
|
594
|
+
fields[key] = value;
|
|
595
|
+
}
|
|
596
|
+
const stage = fields["stage"] ?? "";
|
|
597
|
+
const waveRaw = fields["wave"] ?? "1";
|
|
598
|
+
const waveNum = Number(waveRaw);
|
|
599
|
+
if (!Number.isFinite(waveNum)) {
|
|
600
|
+
throw new ParseError(`<position> wave not numeric: ${waveRaw}`, startLine);
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
stage,
|
|
604
|
+
wave: waveNum,
|
|
605
|
+
task_progress: fields["task_progress"] ?? "0/0",
|
|
606
|
+
status: fields["status"] ?? "initialized",
|
|
607
|
+
handoff_source: fields["handoff_source"] ?? "",
|
|
608
|
+
handoff_path: fields["handoff_path"] ?? "",
|
|
609
|
+
skipped_stages: fields["skipped_stages"] ?? ""
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function parseDecisionsBody(body, startLine) {
|
|
613
|
+
const out = [];
|
|
614
|
+
const lines = body.split("\n");
|
|
615
|
+
const re = /^(D-\d+):\s*(.*?)\s*\((locked|tentative)\)\s*$/;
|
|
616
|
+
for (let i = 0; i < lines.length; i++) {
|
|
617
|
+
const line = (lines[i] ?? "").trim();
|
|
618
|
+
if (line === "" || line.startsWith("<!--")) continue;
|
|
619
|
+
const m = line.match(re);
|
|
620
|
+
if (!m) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const id = m[1] ?? "";
|
|
624
|
+
const text = m[2] ?? "";
|
|
625
|
+
const status = m[3];
|
|
626
|
+
if (!isDecisionStatus(status)) {
|
|
627
|
+
throw new ParseError(
|
|
628
|
+
`<decisions> invalid status for ${id}: ${status}`,
|
|
629
|
+
startLine + i
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
out.push({ id, text, status });
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
function parseMustHavesBody(body, startLine) {
|
|
637
|
+
const out = [];
|
|
638
|
+
const lines = body.split("\n");
|
|
639
|
+
const re = /^(M-\d+):\s*(.*?)\s*\|\s*status:\s*(pending|pass|fail)\s*$/;
|
|
640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
641
|
+
const line = (lines[i] ?? "").trim();
|
|
642
|
+
if (line === "" || line.startsWith("<!--")) continue;
|
|
643
|
+
const m = line.match(re);
|
|
644
|
+
if (!m) continue;
|
|
645
|
+
const id = m[1] ?? "";
|
|
646
|
+
const text = m[2] ?? "";
|
|
647
|
+
const status = m[3];
|
|
648
|
+
if (!isMustHaveStatus(status)) {
|
|
649
|
+
throw new ParseError(
|
|
650
|
+
`<must_haves> invalid status for ${id}: ${status}`,
|
|
651
|
+
startLine + i
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
out.push({ id, text, status });
|
|
655
|
+
}
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
function parseConnectionsBody(body, startLine) {
|
|
659
|
+
const out = {};
|
|
660
|
+
const lines = body.split("\n");
|
|
661
|
+
for (let i = 0; i < lines.length; i++) {
|
|
662
|
+
const line = lines[i] ?? "";
|
|
663
|
+
const trimmed = line.trim();
|
|
664
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
665
|
+
const idx = line.indexOf(":");
|
|
666
|
+
if (idx === -1) continue;
|
|
667
|
+
const key = line.slice(0, idx).trim();
|
|
668
|
+
const value = line.slice(idx + 1).trim();
|
|
669
|
+
if (!isConnectionStatus(value)) {
|
|
670
|
+
throw new ParseError(
|
|
671
|
+
`<connections> invalid status for ${key}: ${value}`,
|
|
672
|
+
startLine + i
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
out[key] = value;
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function parseBlockersBody(body, startLine) {
|
|
680
|
+
const out = [];
|
|
681
|
+
const lines = body.split("\n");
|
|
682
|
+
const re = /^\[([^\]]+)\]\s*\[([^\]]+)\]:\s*(.*)$/;
|
|
683
|
+
for (let i = 0; i < lines.length; i++) {
|
|
684
|
+
const line = (lines[i] ?? "").trim();
|
|
685
|
+
if (line === "" || line.startsWith("<!--")) continue;
|
|
686
|
+
const m = line.match(re);
|
|
687
|
+
if (!m) {
|
|
688
|
+
throw new ParseError(
|
|
689
|
+
`<blockers> malformed line: "${line}"`,
|
|
690
|
+
startLine + i
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
out.push({ stage: m[1] ?? "", date: m[2] ?? "", text: m[3] ?? "" });
|
|
694
|
+
}
|
|
695
|
+
return out;
|
|
696
|
+
}
|
|
697
|
+
function parsePrototypingBody(body, startLine) {
|
|
698
|
+
const sketches = [];
|
|
699
|
+
const spikes = [];
|
|
700
|
+
const skipped = [];
|
|
701
|
+
const lines = body.split("\n");
|
|
702
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
703
|
+
for (let i = 0; i < lines.length; i++) {
|
|
704
|
+
const line = (lines[i] ?? "").trim();
|
|
705
|
+
if (line === "" || line.startsWith("<!--")) continue;
|
|
706
|
+
const m = line.match(selfClose);
|
|
707
|
+
if (!m) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const tag = m[1] ?? "";
|
|
711
|
+
const attrs = parsePrototypingAttrs(m[2] ?? "");
|
|
712
|
+
const fileLine = startLine + i;
|
|
713
|
+
if (tag === "sketch") {
|
|
714
|
+
sketches.push(buildSketchEntry(attrs, fileLine));
|
|
715
|
+
} else if (tag === "spike") {
|
|
716
|
+
spikes.push(buildSpikeEntry(attrs, fileLine));
|
|
717
|
+
} else if (tag === "skipped") {
|
|
718
|
+
skipped.push(buildSkippedEntry(attrs, fileLine));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return { sketches, spikes, skipped };
|
|
722
|
+
}
|
|
723
|
+
function parsePrototypingAttrs(span) {
|
|
724
|
+
const out = {};
|
|
725
|
+
const re = /([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+))/g;
|
|
726
|
+
let m;
|
|
727
|
+
while ((m = re.exec(span)) !== null) {
|
|
728
|
+
const key = m[1] ?? "";
|
|
729
|
+
const value = (m[2] !== void 0 ? m[2] : void 0) ?? (m[3] !== void 0 ? m[3] : void 0) ?? m[4] ?? "";
|
|
730
|
+
if (key !== "") out[key] = value;
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
function buildSketchEntry(attrs, fileLine) {
|
|
735
|
+
const slug = attrs["slug"];
|
|
736
|
+
const cycle = attrs["cycle"];
|
|
737
|
+
const decision = attrs["decision"];
|
|
738
|
+
const status = attrs["status"] ?? "resolved";
|
|
739
|
+
if (slug === void 0) {
|
|
740
|
+
throw new ParseError("<sketch/> missing required attribute slug", fileLine);
|
|
741
|
+
}
|
|
742
|
+
if (cycle === void 0) {
|
|
743
|
+
throw new ParseError("<sketch/> missing required attribute cycle", fileLine);
|
|
744
|
+
}
|
|
745
|
+
if (decision === void 0) {
|
|
746
|
+
throw new ParseError(
|
|
747
|
+
"<sketch/> missing required attribute decision",
|
|
748
|
+
fileLine
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (!isPrototypingEntryStatus(status)) {
|
|
752
|
+
throw new ParseError(
|
|
753
|
+
`<sketch/> invalid status: ${status}`,
|
|
754
|
+
fileLine
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
slug,
|
|
759
|
+
cycle,
|
|
760
|
+
decision,
|
|
761
|
+
status,
|
|
762
|
+
extra_attrs: extractExtraAttrs(attrs, ["slug", "cycle", "decision", "status"])
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function buildSpikeEntry(attrs, fileLine) {
|
|
766
|
+
const slug = attrs["slug"];
|
|
767
|
+
const cycle = attrs["cycle"];
|
|
768
|
+
const decision = attrs["decision"];
|
|
769
|
+
const verdict = attrs["verdict"];
|
|
770
|
+
const status = attrs["status"] ?? "resolved";
|
|
771
|
+
if (slug === void 0) {
|
|
772
|
+
throw new ParseError("<spike/> missing required attribute slug", fileLine);
|
|
773
|
+
}
|
|
774
|
+
if (cycle === void 0) {
|
|
775
|
+
throw new ParseError("<spike/> missing required attribute cycle", fileLine);
|
|
776
|
+
}
|
|
777
|
+
if (decision === void 0) {
|
|
778
|
+
throw new ParseError(
|
|
779
|
+
"<spike/> missing required attribute decision",
|
|
780
|
+
fileLine
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (verdict === void 0) {
|
|
784
|
+
throw new ParseError(
|
|
785
|
+
"<spike/> missing required attribute verdict",
|
|
786
|
+
fileLine
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
if (!isSpikeVerdict(verdict)) {
|
|
790
|
+
throw new ParseError(
|
|
791
|
+
`<spike/> invalid verdict: ${verdict}`,
|
|
792
|
+
fileLine
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (!isPrototypingEntryStatus(status)) {
|
|
796
|
+
throw new ParseError(
|
|
797
|
+
`<spike/> invalid status: ${status}`,
|
|
798
|
+
fileLine
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
slug,
|
|
803
|
+
cycle,
|
|
804
|
+
decision,
|
|
805
|
+
verdict,
|
|
806
|
+
status,
|
|
807
|
+
extra_attrs: extractExtraAttrs(attrs, [
|
|
808
|
+
"slug",
|
|
809
|
+
"cycle",
|
|
810
|
+
"decision",
|
|
811
|
+
"verdict",
|
|
812
|
+
"status"
|
|
813
|
+
])
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function buildSkippedEntry(attrs, fileLine) {
|
|
817
|
+
const at = attrs["at"];
|
|
818
|
+
const cycle = attrs["cycle"];
|
|
819
|
+
const reason = attrs["reason"];
|
|
820
|
+
if (at === void 0) {
|
|
821
|
+
throw new ParseError("<skipped/> missing required attribute at", fileLine);
|
|
822
|
+
}
|
|
823
|
+
if (cycle === void 0) {
|
|
824
|
+
throw new ParseError(
|
|
825
|
+
"<skipped/> missing required attribute cycle",
|
|
826
|
+
fileLine
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (reason === void 0) {
|
|
830
|
+
throw new ParseError(
|
|
831
|
+
"<skipped/> missing required attribute reason",
|
|
832
|
+
fileLine
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
at,
|
|
837
|
+
cycle,
|
|
838
|
+
reason,
|
|
839
|
+
extra_attrs: extractExtraAttrs(attrs, ["at", "cycle", "reason"])
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function extractExtraAttrs(all, known) {
|
|
843
|
+
const out = {};
|
|
844
|
+
for (const [k, v] of Object.entries(all)) {
|
|
845
|
+
if (!known.includes(k)) out[k] = v;
|
|
846
|
+
}
|
|
847
|
+
return out;
|
|
848
|
+
}
|
|
849
|
+
function parseQualityGateBody(body, startLine) {
|
|
850
|
+
let run = null;
|
|
851
|
+
const lines = body.split("\n");
|
|
852
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
853
|
+
for (let i = 0; i < lines.length; i++) {
|
|
854
|
+
const line = (lines[i] ?? "").trim();
|
|
855
|
+
if (line === "" || line.startsWith("<!--")) continue;
|
|
856
|
+
const m = line.match(selfClose);
|
|
857
|
+
if (!m) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const tag = m[1] ?? "";
|
|
861
|
+
if (tag !== "run") continue;
|
|
862
|
+
const attrs = parsePrototypingAttrs(m[2] ?? "");
|
|
863
|
+
run = buildQualityGateRun(attrs, startLine + i);
|
|
864
|
+
}
|
|
865
|
+
return { run };
|
|
866
|
+
}
|
|
867
|
+
function buildQualityGateRun(attrs, fileLine) {
|
|
868
|
+
const started_at = attrs["started_at"];
|
|
869
|
+
const completed_at = attrs["completed_at"];
|
|
870
|
+
const status = attrs["status"];
|
|
871
|
+
const iterationRaw = attrs["iteration"];
|
|
872
|
+
const commands_run = attrs["commands_run"];
|
|
873
|
+
if (started_at === void 0) {
|
|
874
|
+
throw new ParseError(
|
|
875
|
+
"<quality_gate> <run/> missing required attribute started_at",
|
|
876
|
+
fileLine
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (completed_at === void 0) {
|
|
880
|
+
throw new ParseError(
|
|
881
|
+
"<quality_gate> <run/> missing required attribute completed_at",
|
|
882
|
+
fileLine
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (status === void 0) {
|
|
886
|
+
throw new ParseError(
|
|
887
|
+
"<quality_gate> <run/> missing required attribute status",
|
|
888
|
+
fileLine
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
if (!isQualityGateStatus(status)) {
|
|
892
|
+
throw new ParseError(
|
|
893
|
+
`<quality_gate> <run/> invalid status: ${status}`,
|
|
894
|
+
fileLine
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (iterationRaw === void 0) {
|
|
898
|
+
throw new ParseError(
|
|
899
|
+
"<quality_gate> <run/> missing required attribute iteration",
|
|
900
|
+
fileLine
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const iteration = Number(iterationRaw);
|
|
904
|
+
if (!Number.isFinite(iteration) || !Number.isInteger(iteration) || iteration < 0) {
|
|
905
|
+
throw new ParseError(
|
|
906
|
+
`<quality_gate> <run/> iteration not a non-negative integer: ${iterationRaw}`,
|
|
907
|
+
fileLine
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
if (commands_run === void 0) {
|
|
911
|
+
throw new ParseError(
|
|
912
|
+
"<quality_gate> <run/> missing required attribute commands_run",
|
|
913
|
+
fileLine
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
started_at,
|
|
918
|
+
completed_at,
|
|
919
|
+
status,
|
|
920
|
+
iteration,
|
|
921
|
+
commands_run,
|
|
922
|
+
extra_attrs: extractExtraAttrs(attrs, [
|
|
923
|
+
"started_at",
|
|
924
|
+
"completed_at",
|
|
925
|
+
"status",
|
|
926
|
+
"iteration",
|
|
927
|
+
"commands_run"
|
|
928
|
+
])
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function parseTimestampsBody(body, _startLine) {
|
|
932
|
+
const out = {};
|
|
933
|
+
const lines = body.split("\n");
|
|
934
|
+
for (const line of lines) {
|
|
935
|
+
const trimmed = line.trim();
|
|
936
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
937
|
+
const idx = line.indexOf(":");
|
|
938
|
+
if (idx === -1) continue;
|
|
939
|
+
const key = line.slice(0, idx).trim();
|
|
940
|
+
const value = line.slice(idx + 1).trim();
|
|
941
|
+
out[key] = value;
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// sdk/state/mutator.ts
|
|
947
|
+
function serialize(state, fidelity = {}) {
|
|
948
|
+
const {
|
|
949
|
+
raw_frontmatter,
|
|
950
|
+
raw_bodies,
|
|
951
|
+
block_gaps,
|
|
952
|
+
line_ending = "\n"
|
|
953
|
+
} = fidelity;
|
|
954
|
+
const out = [];
|
|
955
|
+
out.push("---\n");
|
|
956
|
+
out.push(emitFrontmatter(state.frontmatter, raw_frontmatter));
|
|
957
|
+
out.push("---\n");
|
|
958
|
+
for (const name12 of BLOCK_ORDER) {
|
|
959
|
+
const rawBody = raw_bodies?.[name12] ?? null;
|
|
960
|
+
const emitted = emitBlock(name12, state, rawBody);
|
|
961
|
+
if (emitted === null) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const gap = block_gaps?.[name12] ?? (out.length > 0 && isFirstEmission(out) ? "" : "\n");
|
|
965
|
+
out.push(gap);
|
|
966
|
+
out.push(`<${name12}>
|
|
967
|
+
`);
|
|
968
|
+
out.push(emitted);
|
|
969
|
+
if (!emitted.endsWith("\n")) out.push("\n");
|
|
970
|
+
out.push(`</${name12}>
|
|
971
|
+
`);
|
|
972
|
+
}
|
|
973
|
+
out.push(state.body_trailer);
|
|
974
|
+
const joined = out.join("");
|
|
975
|
+
return line_ending === "\r\n" ? joined.replace(/\n/g, "\r\n") : joined;
|
|
976
|
+
}
|
|
977
|
+
function isFirstEmission(out) {
|
|
978
|
+
return out.length <= 3;
|
|
979
|
+
}
|
|
980
|
+
function emitFrontmatter(fm, raw_frontmatter) {
|
|
981
|
+
if (raw_frontmatter !== void 0) {
|
|
982
|
+
const reparsed = tryReparseFrontmatter(raw_frontmatter);
|
|
983
|
+
if (reparsed !== null && frontmatterEqual(reparsed, fm)) {
|
|
984
|
+
return raw_frontmatter + "\n";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return canonicalFrontmatter(fm);
|
|
988
|
+
}
|
|
989
|
+
function tryReparseFrontmatter(raw) {
|
|
990
|
+
try {
|
|
991
|
+
const out = {};
|
|
992
|
+
for (const line of raw.split("\n")) {
|
|
993
|
+
const trimmed = line.trim();
|
|
994
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
995
|
+
const idx = line.indexOf(":");
|
|
996
|
+
if (idx === -1) continue;
|
|
997
|
+
const key = line.slice(0, idx).trim();
|
|
998
|
+
let value = line.slice(idx + 1).trim();
|
|
999
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1000
|
+
value = value.slice(1, -1);
|
|
1001
|
+
}
|
|
1002
|
+
if (key === "wave") {
|
|
1003
|
+
const n = Number(value);
|
|
1004
|
+
out[key] = Number.isFinite(n) ? n : value;
|
|
1005
|
+
} else {
|
|
1006
|
+
out[key] = value;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const fm = {
|
|
1010
|
+
pipeline_state_version: String(out["pipeline_state_version"] ?? "1.0"),
|
|
1011
|
+
stage: String(out["stage"] ?? ""),
|
|
1012
|
+
cycle: String(out["cycle"] ?? ""),
|
|
1013
|
+
wave: typeof out["wave"] === "number" ? out["wave"] : 1,
|
|
1014
|
+
started_at: String(out["started_at"] ?? ""),
|
|
1015
|
+
last_checkpoint: String(out["last_checkpoint"] ?? "")
|
|
1016
|
+
};
|
|
1017
|
+
for (const [k, v] of Object.entries(out)) {
|
|
1018
|
+
if (!(k in fm)) fm[k] = v;
|
|
1019
|
+
}
|
|
1020
|
+
return fm;
|
|
1021
|
+
} catch {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function frontmatterEqual(a, b) {
|
|
1026
|
+
const ak = Object.keys(a);
|
|
1027
|
+
const bk = Object.keys(b);
|
|
1028
|
+
if (ak.length !== bk.length) return false;
|
|
1029
|
+
for (const k of ak) {
|
|
1030
|
+
if (!(k in b)) return false;
|
|
1031
|
+
if (a[k] !== b[k]) {
|
|
1032
|
+
if (String(a[k]) !== String(b[k])) return false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
function canonicalFrontmatter(fm) {
|
|
1038
|
+
const fixed = [
|
|
1039
|
+
"pipeline_state_version",
|
|
1040
|
+
"stage",
|
|
1041
|
+
"cycle",
|
|
1042
|
+
"wave",
|
|
1043
|
+
"started_at",
|
|
1044
|
+
"last_checkpoint"
|
|
1045
|
+
];
|
|
1046
|
+
const lines = [];
|
|
1047
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
1048
|
+
for (const k of fixed) {
|
|
1049
|
+
if (k in fm) {
|
|
1050
|
+
lines.push(`${k}: ${formatFrontmatterValue(fm[k])}`);
|
|
1051
|
+
emitted.add(k);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
for (const k of Object.keys(fm)) {
|
|
1055
|
+
if (emitted.has(k)) continue;
|
|
1056
|
+
lines.push(`${k}: ${formatFrontmatterValue(fm[k])}`);
|
|
1057
|
+
}
|
|
1058
|
+
return lines.join("\n") + "\n";
|
|
1059
|
+
}
|
|
1060
|
+
function formatFrontmatterValue(v) {
|
|
1061
|
+
if (v === null || v === void 0) return "";
|
|
1062
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
1063
|
+
if (typeof v === "string") return v;
|
|
1064
|
+
return JSON.stringify(v);
|
|
1065
|
+
}
|
|
1066
|
+
function emitBlock(name12, state, rawBody) {
|
|
1067
|
+
switch (name12) {
|
|
1068
|
+
case "position":
|
|
1069
|
+
return emitPosition(state.position, rawBody);
|
|
1070
|
+
case "decisions":
|
|
1071
|
+
return emitDecisions(state.decisions, rawBody);
|
|
1072
|
+
case "must_haves":
|
|
1073
|
+
return emitMustHaves(state.must_haves, rawBody);
|
|
1074
|
+
case "prototyping":
|
|
1075
|
+
return emitPrototyping(state.prototyping, rawBody);
|
|
1076
|
+
case "quality_gate":
|
|
1077
|
+
return emitQualityGate(state.quality_gate, rawBody);
|
|
1078
|
+
case "connections":
|
|
1079
|
+
return emitConnections(state.connections, rawBody);
|
|
1080
|
+
case "blockers":
|
|
1081
|
+
return emitBlockers(state.blockers, rawBody);
|
|
1082
|
+
case "parallelism_decision":
|
|
1083
|
+
if (rawBody !== null) {
|
|
1084
|
+
if (state.parallelism_decision === rawBody) return rawBody;
|
|
1085
|
+
return state.parallelism_decision ?? "";
|
|
1086
|
+
}
|
|
1087
|
+
return state.parallelism_decision;
|
|
1088
|
+
case "todos":
|
|
1089
|
+
if (rawBody !== null) {
|
|
1090
|
+
if (state.todos === rawBody) return rawBody;
|
|
1091
|
+
return state.todos ?? "";
|
|
1092
|
+
}
|
|
1093
|
+
return state.todos;
|
|
1094
|
+
case "timestamps":
|
|
1095
|
+
return emitTimestamps(state.timestamps, rawBody);
|
|
1096
|
+
default: {
|
|
1097
|
+
const _exhaustive = name12;
|
|
1098
|
+
void _exhaustive;
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function emitPosition(pos, rawBody) {
|
|
1104
|
+
if (rawBody !== null) {
|
|
1105
|
+
const reparsed = tryReparsePosition(rawBody);
|
|
1106
|
+
if (reparsed !== null && positionEqual(reparsed, pos)) return rawBody;
|
|
1107
|
+
}
|
|
1108
|
+
return [
|
|
1109
|
+
`stage: ${pos.stage}`,
|
|
1110
|
+
`wave: ${pos.wave}`,
|
|
1111
|
+
`task_progress: ${pos.task_progress}`,
|
|
1112
|
+
`status: ${pos.status}`,
|
|
1113
|
+
`handoff_source: ${quoteIfEmpty(pos.handoff_source)}`,
|
|
1114
|
+
`handoff_path: ${quoteIfEmpty(pos.handoff_path)}`,
|
|
1115
|
+
`skipped_stages: ${quoteIfEmpty(pos.skipped_stages)}`
|
|
1116
|
+
].join("\n");
|
|
1117
|
+
}
|
|
1118
|
+
function quoteIfEmpty(v) {
|
|
1119
|
+
return v === "" ? '""' : v;
|
|
1120
|
+
}
|
|
1121
|
+
function emitDecisions(decisions, rawBody) {
|
|
1122
|
+
if (rawBody !== null) {
|
|
1123
|
+
const reparsed = tryReparseDecisions(rawBody);
|
|
1124
|
+
if (reparsed !== null && decisionsEqual(reparsed, decisions)) return rawBody;
|
|
1125
|
+
}
|
|
1126
|
+
if (decisions.length === 0) return "";
|
|
1127
|
+
return decisions.map((d) => `${d.id}: ${d.text} (${d.status})`).join("\n");
|
|
1128
|
+
}
|
|
1129
|
+
function emitMustHaves(mh, rawBody) {
|
|
1130
|
+
if (rawBody !== null) {
|
|
1131
|
+
const reparsed = tryReparseMustHaves(rawBody);
|
|
1132
|
+
if (reparsed !== null && mustHavesEqual(reparsed, mh)) return rawBody;
|
|
1133
|
+
}
|
|
1134
|
+
if (mh.length === 0) return "";
|
|
1135
|
+
return mh.map((m) => `${m.id}: ${m.text} | status: ${m.status}`).join("\n");
|
|
1136
|
+
}
|
|
1137
|
+
function emitConnections(conns, rawBody) {
|
|
1138
|
+
if (rawBody !== null) {
|
|
1139
|
+
const reparsed = tryReparseConnections(rawBody);
|
|
1140
|
+
if (reparsed !== null && connectionsEqual(reparsed, conns)) return rawBody;
|
|
1141
|
+
}
|
|
1142
|
+
const keys = Object.keys(conns);
|
|
1143
|
+
if (keys.length === 0) return "";
|
|
1144
|
+
return keys.map((k) => `${k}: ${conns[k]}`).join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
function emitBlockers(blockers, rawBody) {
|
|
1147
|
+
if (rawBody !== null) {
|
|
1148
|
+
const reparsed = tryReparseBlockers(rawBody);
|
|
1149
|
+
if (reparsed !== null && blockersEqual(reparsed, blockers)) return rawBody;
|
|
1150
|
+
}
|
|
1151
|
+
if (blockers.length === 0) return "";
|
|
1152
|
+
return blockers.map((b) => `[${b.stage}] [${b.date}]: ${b.text}`).join("\n");
|
|
1153
|
+
}
|
|
1154
|
+
function emitPrototyping(block, rawBody) {
|
|
1155
|
+
if (block === null && rawBody === null) return null;
|
|
1156
|
+
if (block === null) {
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
if (rawBody !== null) {
|
|
1160
|
+
const reparsed = tryReparsePrototyping(rawBody);
|
|
1161
|
+
if (reparsed !== null && prototypingEqual(reparsed, block)) {
|
|
1162
|
+
return rawBody;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return canonicalPrototyping(block);
|
|
1166
|
+
}
|
|
1167
|
+
function canonicalPrototyping(block) {
|
|
1168
|
+
const lines = [];
|
|
1169
|
+
for (const s of block.sketches) {
|
|
1170
|
+
lines.push(canonicalSketch(s));
|
|
1171
|
+
}
|
|
1172
|
+
for (const sp of block.spikes) {
|
|
1173
|
+
lines.push(canonicalSpike(sp));
|
|
1174
|
+
}
|
|
1175
|
+
for (const sk of block.skipped) {
|
|
1176
|
+
lines.push(canonicalSkipped(sk));
|
|
1177
|
+
}
|
|
1178
|
+
return lines.join("\n");
|
|
1179
|
+
}
|
|
1180
|
+
function canonicalSketch(s) {
|
|
1181
|
+
const parts = [
|
|
1182
|
+
`slug=${formatPrototypingAttr(s.slug)}`,
|
|
1183
|
+
`cycle=${formatPrototypingAttr(s.cycle)}`,
|
|
1184
|
+
`decision=${formatPrototypingAttr(s.decision)}`,
|
|
1185
|
+
`status=${formatPrototypingAttr(s.status)}`
|
|
1186
|
+
];
|
|
1187
|
+
for (const [k, v] of Object.entries(s.extra_attrs)) {
|
|
1188
|
+
parts.push(`${k}=${formatPrototypingAttr(v)}`);
|
|
1189
|
+
}
|
|
1190
|
+
return `<sketch ${parts.join(" ")}/>`;
|
|
1191
|
+
}
|
|
1192
|
+
function canonicalSpike(s) {
|
|
1193
|
+
const parts = [
|
|
1194
|
+
`slug=${formatPrototypingAttr(s.slug)}`,
|
|
1195
|
+
`cycle=${formatPrototypingAttr(s.cycle)}`,
|
|
1196
|
+
`decision=${formatPrototypingAttr(s.decision)}`,
|
|
1197
|
+
`verdict=${formatPrototypingAttr(s.verdict)}`,
|
|
1198
|
+
`status=${formatPrototypingAttr(s.status)}`
|
|
1199
|
+
];
|
|
1200
|
+
for (const [k, v] of Object.entries(s.extra_attrs)) {
|
|
1201
|
+
parts.push(`${k}=${formatPrototypingAttr(v)}`);
|
|
1202
|
+
}
|
|
1203
|
+
return `<spike ${parts.join(" ")}/>`;
|
|
1204
|
+
}
|
|
1205
|
+
function canonicalSkipped(s) {
|
|
1206
|
+
const parts = [
|
|
1207
|
+
`at=${formatPrototypingAttr(s.at)}`,
|
|
1208
|
+
`cycle=${formatPrototypingAttr(s.cycle)}`,
|
|
1209
|
+
`reason=${formatPrototypingAttr(s.reason)}`
|
|
1210
|
+
];
|
|
1211
|
+
for (const [k, v] of Object.entries(s.extra_attrs)) {
|
|
1212
|
+
parts.push(`${k}=${formatPrototypingAttr(v)}`);
|
|
1213
|
+
}
|
|
1214
|
+
return `<skipped ${parts.join(" ")}/>`;
|
|
1215
|
+
}
|
|
1216
|
+
function formatPrototypingAttr(v) {
|
|
1217
|
+
return `"${v.replace(/"/g, """)}"`;
|
|
1218
|
+
}
|
|
1219
|
+
function emitQualityGate(block, rawBody) {
|
|
1220
|
+
if (block === null && rawBody === null) return null;
|
|
1221
|
+
if (block === null) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
if (rawBody !== null) {
|
|
1225
|
+
const reparsed = tryReparseQualityGate(rawBody);
|
|
1226
|
+
if (reparsed !== null && qualityGateEqual(reparsed, block)) {
|
|
1227
|
+
return rawBody;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return canonicalQualityGate(block);
|
|
1231
|
+
}
|
|
1232
|
+
function canonicalQualityGate(block) {
|
|
1233
|
+
if (block.run === null) return "";
|
|
1234
|
+
return canonicalQualityGateRun(block.run);
|
|
1235
|
+
}
|
|
1236
|
+
function canonicalQualityGateRun(run) {
|
|
1237
|
+
const parts = [
|
|
1238
|
+
`started_at=${formatPrototypingAttr(run.started_at)}`,
|
|
1239
|
+
`completed_at=${formatPrototypingAttr(run.completed_at)}`,
|
|
1240
|
+
`status=${formatPrototypingAttr(run.status)}`,
|
|
1241
|
+
`iteration=${formatPrototypingAttr(String(run.iteration))}`,
|
|
1242
|
+
`commands_run=${formatPrototypingAttr(run.commands_run)}`
|
|
1243
|
+
];
|
|
1244
|
+
for (const [k, v] of Object.entries(run.extra_attrs)) {
|
|
1245
|
+
parts.push(`${k}=${formatPrototypingAttr(v)}`);
|
|
1246
|
+
}
|
|
1247
|
+
return `<run ${parts.join(" ")}/>`;
|
|
1248
|
+
}
|
|
1249
|
+
function emitTimestamps(ts, rawBody) {
|
|
1250
|
+
if (rawBody !== null) {
|
|
1251
|
+
const reparsed = tryReparseTimestamps(rawBody);
|
|
1252
|
+
if (reparsed !== null && recordsEqual(reparsed, ts)) return rawBody;
|
|
1253
|
+
}
|
|
1254
|
+
const keys = Object.keys(ts);
|
|
1255
|
+
if (keys.length === 0) return "";
|
|
1256
|
+
return keys.map((k) => `${k}: ${ts[k]}`).join("\n");
|
|
1257
|
+
}
|
|
1258
|
+
function positionEqual(a, b) {
|
|
1259
|
+
return a.stage === b.stage && a.wave === b.wave && a.task_progress === b.task_progress && a.status === b.status && a.handoff_source === b.handoff_source && a.handoff_path === b.handoff_path && a.skipped_stages === b.skipped_stages;
|
|
1260
|
+
}
|
|
1261
|
+
function decisionsEqual(a, b) {
|
|
1262
|
+
if (a.length !== b.length) return false;
|
|
1263
|
+
for (let i = 0; i < a.length; i++) {
|
|
1264
|
+
const x = a[i];
|
|
1265
|
+
const y = b[i];
|
|
1266
|
+
if (x === void 0 || y === void 0) return false;
|
|
1267
|
+
if (x.id !== y.id || x.text !== y.text || x.status !== y.status) return false;
|
|
1268
|
+
}
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
function mustHavesEqual(a, b) {
|
|
1272
|
+
if (a.length !== b.length) return false;
|
|
1273
|
+
for (let i = 0; i < a.length; i++) {
|
|
1274
|
+
const x = a[i];
|
|
1275
|
+
const y = b[i];
|
|
1276
|
+
if (x === void 0 || y === void 0) return false;
|
|
1277
|
+
if (x.id !== y.id || x.text !== y.text || x.status !== y.status) return false;
|
|
1278
|
+
}
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
function connectionsEqual(a, b) {
|
|
1282
|
+
const ak = Object.keys(a);
|
|
1283
|
+
const bk = Object.keys(b);
|
|
1284
|
+
if (ak.length !== bk.length) return false;
|
|
1285
|
+
for (let i = 0; i < ak.length; i++) {
|
|
1286
|
+
if (ak[i] !== bk[i]) return false;
|
|
1287
|
+
const key = ak[i];
|
|
1288
|
+
if (a[key] !== b[key]) return false;
|
|
1289
|
+
}
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
function blockersEqual(a, b) {
|
|
1293
|
+
if (a.length !== b.length) return false;
|
|
1294
|
+
for (let i = 0; i < a.length; i++) {
|
|
1295
|
+
const x = a[i];
|
|
1296
|
+
const y = b[i];
|
|
1297
|
+
if (x === void 0 || y === void 0) return false;
|
|
1298
|
+
if (x.stage !== y.stage || x.date !== y.date || x.text !== y.text) return false;
|
|
1299
|
+
}
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
function prototypingEqual(a, b) {
|
|
1303
|
+
if (a.sketches.length !== b.sketches.length) return false;
|
|
1304
|
+
if (a.spikes.length !== b.spikes.length) return false;
|
|
1305
|
+
if (a.skipped.length !== b.skipped.length) return false;
|
|
1306
|
+
for (let i = 0; i < a.sketches.length; i++) {
|
|
1307
|
+
if (!sketchEqual(a.sketches[i], b.sketches[i])) return false;
|
|
1308
|
+
}
|
|
1309
|
+
for (let i = 0; i < a.spikes.length; i++) {
|
|
1310
|
+
if (!spikeEqual(a.spikes[i], b.spikes[i])) return false;
|
|
1311
|
+
}
|
|
1312
|
+
for (let i = 0; i < a.skipped.length; i++) {
|
|
1313
|
+
if (!skippedEqual(a.skipped[i], b.skipped[i])) return false;
|
|
1314
|
+
}
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
function sketchEqual(a, b) {
|
|
1318
|
+
return a.slug === b.slug && a.cycle === b.cycle && a.decision === b.decision && a.status === b.status && extraAttrsEqual(a.extra_attrs, b.extra_attrs);
|
|
1319
|
+
}
|
|
1320
|
+
function spikeEqual(a, b) {
|
|
1321
|
+
return a.slug === b.slug && a.cycle === b.cycle && a.decision === b.decision && a.verdict === b.verdict && a.status === b.status && extraAttrsEqual(a.extra_attrs, b.extra_attrs);
|
|
1322
|
+
}
|
|
1323
|
+
function skippedEqual(a, b) {
|
|
1324
|
+
return a.at === b.at && a.cycle === b.cycle && a.reason === b.reason && extraAttrsEqual(a.extra_attrs, b.extra_attrs);
|
|
1325
|
+
}
|
|
1326
|
+
function extraAttrsEqual(a, b) {
|
|
1327
|
+
const ak = Object.keys(a);
|
|
1328
|
+
const bk = Object.keys(b);
|
|
1329
|
+
if (ak.length !== bk.length) return false;
|
|
1330
|
+
for (let i = 0; i < ak.length; i++) {
|
|
1331
|
+
if (ak[i] !== bk[i]) return false;
|
|
1332
|
+
const key = ak[i];
|
|
1333
|
+
if (a[key] !== b[key]) return false;
|
|
1334
|
+
}
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
function qualityGateEqual(a, b) {
|
|
1338
|
+
if (a.run === null && b.run === null) return true;
|
|
1339
|
+
if (a.run === null || b.run === null) return false;
|
|
1340
|
+
return qualityGateRunEqual(a.run, b.run);
|
|
1341
|
+
}
|
|
1342
|
+
function qualityGateRunEqual(a, b) {
|
|
1343
|
+
return a.started_at === b.started_at && a.completed_at === b.completed_at && a.status === b.status && a.iteration === b.iteration && a.commands_run === b.commands_run && extraAttrsEqual(a.extra_attrs, b.extra_attrs);
|
|
1344
|
+
}
|
|
1345
|
+
function recordsEqual(a, b) {
|
|
1346
|
+
const ak = Object.keys(a);
|
|
1347
|
+
const bk = Object.keys(b);
|
|
1348
|
+
if (ak.length !== bk.length) return false;
|
|
1349
|
+
for (let i = 0; i < ak.length; i++) {
|
|
1350
|
+
if (ak[i] !== bk[i]) return false;
|
|
1351
|
+
const key = ak[i];
|
|
1352
|
+
if (a[key] !== b[key]) return false;
|
|
1353
|
+
}
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
function tryReparsePosition(raw) {
|
|
1357
|
+
try {
|
|
1358
|
+
const fields = {};
|
|
1359
|
+
for (const line of raw.split("\n")) {
|
|
1360
|
+
const trimmed = line.trim();
|
|
1361
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
1362
|
+
const idx = line.indexOf(":");
|
|
1363
|
+
if (idx === -1) continue;
|
|
1364
|
+
const key = line.slice(0, idx).trim();
|
|
1365
|
+
let value = line.slice(idx + 1).trim();
|
|
1366
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1367
|
+
value = value.slice(1, -1);
|
|
1368
|
+
}
|
|
1369
|
+
fields[key] = value;
|
|
1370
|
+
}
|
|
1371
|
+
const waveNum = Number(fields["wave"] ?? "1");
|
|
1372
|
+
if (!Number.isFinite(waveNum)) return null;
|
|
1373
|
+
return {
|
|
1374
|
+
stage: fields["stage"] ?? "",
|
|
1375
|
+
wave: waveNum,
|
|
1376
|
+
task_progress: fields["task_progress"] ?? "0/0",
|
|
1377
|
+
status: fields["status"] ?? "initialized",
|
|
1378
|
+
handoff_source: fields["handoff_source"] ?? "",
|
|
1379
|
+
handoff_path: fields["handoff_path"] ?? "",
|
|
1380
|
+
skipped_stages: fields["skipped_stages"] ?? ""
|
|
1381
|
+
};
|
|
1382
|
+
} catch {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function tryReparseDecisions(raw) {
|
|
1387
|
+
try {
|
|
1388
|
+
const out = [];
|
|
1389
|
+
const re = /^(D-\d+):\s*(.*?)\s*\((locked|tentative)\)\s*$/;
|
|
1390
|
+
for (const line of raw.split("\n")) {
|
|
1391
|
+
const t = line.trim();
|
|
1392
|
+
if (t === "" || t.startsWith("<!--")) continue;
|
|
1393
|
+
const m = t.match(re);
|
|
1394
|
+
if (!m) continue;
|
|
1395
|
+
out.push({
|
|
1396
|
+
id: m[1] ?? "",
|
|
1397
|
+
text: m[2] ?? "",
|
|
1398
|
+
status: m[3]
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
return out;
|
|
1402
|
+
} catch {
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function tryReparseMustHaves(raw) {
|
|
1407
|
+
try {
|
|
1408
|
+
const out = [];
|
|
1409
|
+
const re = /^(M-\d+):\s*(.*?)\s*\|\s*status:\s*(pending|pass|fail)\s*$/;
|
|
1410
|
+
for (const line of raw.split("\n")) {
|
|
1411
|
+
const t = line.trim();
|
|
1412
|
+
if (t === "" || t.startsWith("<!--")) continue;
|
|
1413
|
+
const m = t.match(re);
|
|
1414
|
+
if (!m) continue;
|
|
1415
|
+
out.push({
|
|
1416
|
+
id: m[1] ?? "",
|
|
1417
|
+
text: m[2] ?? "",
|
|
1418
|
+
status: m[3]
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return out;
|
|
1422
|
+
} catch {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function tryReparseConnections(raw) {
|
|
1427
|
+
try {
|
|
1428
|
+
const out = {};
|
|
1429
|
+
for (const line of raw.split("\n")) {
|
|
1430
|
+
const trimmed = line.trim();
|
|
1431
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
1432
|
+
const idx = line.indexOf(":");
|
|
1433
|
+
if (idx === -1) continue;
|
|
1434
|
+
const key = line.slice(0, idx).trim();
|
|
1435
|
+
const value = line.slice(idx + 1).trim();
|
|
1436
|
+
if (value !== "available" && value !== "unavailable" && value !== "not_configured") {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
out[key] = value;
|
|
1440
|
+
}
|
|
1441
|
+
return out;
|
|
1442
|
+
} catch {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function tryReparseBlockers(raw) {
|
|
1447
|
+
try {
|
|
1448
|
+
const out = [];
|
|
1449
|
+
const re = /^\[([^\]]+)\]\s*\[([^\]]+)\]:\s*(.*)$/;
|
|
1450
|
+
for (const line of raw.split("\n")) {
|
|
1451
|
+
const t = line.trim();
|
|
1452
|
+
if (t === "" || t.startsWith("<!--")) continue;
|
|
1453
|
+
const m = t.match(re);
|
|
1454
|
+
if (!m) return null;
|
|
1455
|
+
out.push({ stage: m[1] ?? "", date: m[2] ?? "", text: m[3] ?? "" });
|
|
1456
|
+
}
|
|
1457
|
+
return out;
|
|
1458
|
+
} catch {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function tryReparsePrototyping(raw) {
|
|
1463
|
+
try {
|
|
1464
|
+
const sketches = [];
|
|
1465
|
+
const spikes = [];
|
|
1466
|
+
const skipped = [];
|
|
1467
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
1468
|
+
for (const line of raw.split("\n")) {
|
|
1469
|
+
const t = line.trim();
|
|
1470
|
+
if (t === "" || t.startsWith("<!--")) continue;
|
|
1471
|
+
const m = t.match(selfClose);
|
|
1472
|
+
if (!m) {
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const tag = m[1] ?? "";
|
|
1476
|
+
const attrs = parseAttrInline(m[2] ?? "");
|
|
1477
|
+
if (tag === "sketch") {
|
|
1478
|
+
const slug = attrs["slug"];
|
|
1479
|
+
const cycle = attrs["cycle"];
|
|
1480
|
+
const decision = attrs["decision"];
|
|
1481
|
+
const status = attrs["status"] ?? "resolved";
|
|
1482
|
+
if (slug === void 0 || cycle === void 0 || decision === void 0) {
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
if (status !== "resolved") return null;
|
|
1486
|
+
sketches.push({
|
|
1487
|
+
slug,
|
|
1488
|
+
cycle,
|
|
1489
|
+
decision,
|
|
1490
|
+
status: "resolved",
|
|
1491
|
+
extra_attrs: extractExtras(attrs, [
|
|
1492
|
+
"slug",
|
|
1493
|
+
"cycle",
|
|
1494
|
+
"decision",
|
|
1495
|
+
"status"
|
|
1496
|
+
])
|
|
1497
|
+
});
|
|
1498
|
+
} else if (tag === "spike") {
|
|
1499
|
+
const slug = attrs["slug"];
|
|
1500
|
+
const cycle = attrs["cycle"];
|
|
1501
|
+
const decision = attrs["decision"];
|
|
1502
|
+
const verdict = attrs["verdict"];
|
|
1503
|
+
const status = attrs["status"] ?? "resolved";
|
|
1504
|
+
if (slug === void 0 || cycle === void 0 || decision === void 0 || verdict === void 0) {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
if (verdict !== "yes" && verdict !== "no" && verdict !== "partial") {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
if (status !== "resolved") return null;
|
|
1511
|
+
spikes.push({
|
|
1512
|
+
slug,
|
|
1513
|
+
cycle,
|
|
1514
|
+
decision,
|
|
1515
|
+
verdict,
|
|
1516
|
+
status: "resolved",
|
|
1517
|
+
extra_attrs: extractExtras(attrs, [
|
|
1518
|
+
"slug",
|
|
1519
|
+
"cycle",
|
|
1520
|
+
"decision",
|
|
1521
|
+
"verdict",
|
|
1522
|
+
"status"
|
|
1523
|
+
])
|
|
1524
|
+
});
|
|
1525
|
+
} else if (tag === "skipped") {
|
|
1526
|
+
const at = attrs["at"];
|
|
1527
|
+
const cycle = attrs["cycle"];
|
|
1528
|
+
const reason = attrs["reason"];
|
|
1529
|
+
if (at === void 0 || cycle === void 0 || reason === void 0) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
skipped.push({
|
|
1533
|
+
at,
|
|
1534
|
+
cycle,
|
|
1535
|
+
reason,
|
|
1536
|
+
extra_attrs: extractExtras(attrs, ["at", "cycle", "reason"])
|
|
1537
|
+
});
|
|
1538
|
+
} else {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return { sketches, spikes, skipped };
|
|
1543
|
+
} catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function parseAttrInline(span) {
|
|
1548
|
+
const out = {};
|
|
1549
|
+
const re = /([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s/>]+))/g;
|
|
1550
|
+
let m;
|
|
1551
|
+
while ((m = re.exec(span)) !== null) {
|
|
1552
|
+
const key = m[1] ?? "";
|
|
1553
|
+
const value = (m[2] !== void 0 ? m[2] : void 0) ?? (m[3] !== void 0 ? m[3] : void 0) ?? m[4] ?? "";
|
|
1554
|
+
if (key !== "") out[key] = value;
|
|
1555
|
+
}
|
|
1556
|
+
return out;
|
|
1557
|
+
}
|
|
1558
|
+
function extractExtras(all, known) {
|
|
1559
|
+
const out = {};
|
|
1560
|
+
for (const [k, v] of Object.entries(all)) {
|
|
1561
|
+
if (!known.includes(k)) out[k] = v;
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1565
|
+
function tryReparseQualityGate(raw) {
|
|
1566
|
+
try {
|
|
1567
|
+
let run = null;
|
|
1568
|
+
const selfClose = /^<([a-z_]+)(\s+[^>]*?)?\s*\/>\s*$/;
|
|
1569
|
+
for (const line of raw.split("\n")) {
|
|
1570
|
+
const t = line.trim();
|
|
1571
|
+
if (t === "" || t.startsWith("<!--")) continue;
|
|
1572
|
+
const m = t.match(selfClose);
|
|
1573
|
+
if (!m) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
const tag = m[1] ?? "";
|
|
1577
|
+
if (tag !== "run") {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const attrs = parseAttrInline(m[2] ?? "");
|
|
1581
|
+
const started_at = attrs["started_at"];
|
|
1582
|
+
const completed_at = attrs["completed_at"];
|
|
1583
|
+
const status = attrs["status"];
|
|
1584
|
+
const iterationRaw = attrs["iteration"];
|
|
1585
|
+
const commands_run = attrs["commands_run"];
|
|
1586
|
+
if (started_at === void 0 || completed_at === void 0 || status === void 0 || iterationRaw === void 0 || commands_run === void 0) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
if (status !== "pass" && status !== "fail" && status !== "timeout" && status !== "skipped") {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
const iteration = Number(iterationRaw);
|
|
1593
|
+
if (!Number.isFinite(iteration) || !Number.isInteger(iteration) || iteration < 0) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
run = {
|
|
1597
|
+
started_at,
|
|
1598
|
+
completed_at,
|
|
1599
|
+
status,
|
|
1600
|
+
iteration,
|
|
1601
|
+
commands_run,
|
|
1602
|
+
extra_attrs: extractExtras(attrs, [
|
|
1603
|
+
"started_at",
|
|
1604
|
+
"completed_at",
|
|
1605
|
+
"status",
|
|
1606
|
+
"iteration",
|
|
1607
|
+
"commands_run"
|
|
1608
|
+
])
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
return { run };
|
|
1612
|
+
} catch {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function tryReparseTimestamps(raw) {
|
|
1617
|
+
try {
|
|
1618
|
+
const out = {};
|
|
1619
|
+
for (const line of raw.split("\n")) {
|
|
1620
|
+
const trimmed = line.trim();
|
|
1621
|
+
if (trimmed === "" || trimmed.startsWith("<!--")) continue;
|
|
1622
|
+
const idx = line.indexOf(":");
|
|
1623
|
+
if (idx === -1) continue;
|
|
1624
|
+
const key = line.slice(0, idx).trim();
|
|
1625
|
+
const value = line.slice(idx + 1).trim();
|
|
1626
|
+
out[key] = value;
|
|
1627
|
+
}
|
|
1628
|
+
return out;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// sdk/state/gates.ts
|
|
1635
|
+
var DESIGN_KEYWORDS = ["component", "token", "style", "copy", "layout"];
|
|
1636
|
+
function stageParity(state, expected) {
|
|
1637
|
+
if (!state.position || typeof state.position.stage !== "string") {
|
|
1638
|
+
return "position block missing or malformed";
|
|
1639
|
+
}
|
|
1640
|
+
if (!state.frontmatter || typeof state.frontmatter.stage !== "string") {
|
|
1641
|
+
return "frontmatter stage missing or malformed";
|
|
1642
|
+
}
|
|
1643
|
+
if (state.position.stage !== expected) {
|
|
1644
|
+
return `position.stage is "${state.position.stage}" \u2014 expected "${expected}"`;
|
|
1645
|
+
}
|
|
1646
|
+
if (state.frontmatter.stage !== expected) {
|
|
1647
|
+
return `frontmatter.stage is "${state.frontmatter.stage}" \u2014 expected "${expected}"`;
|
|
1648
|
+
}
|
|
1649
|
+
return null;
|
|
1650
|
+
}
|
|
1651
|
+
var briefToExplore = (s) => {
|
|
1652
|
+
const blockers = [];
|
|
1653
|
+
const parity = stageParity(s, "brief");
|
|
1654
|
+
if (parity) blockers.push(parity);
|
|
1655
|
+
return { pass: blockers.length === 0, blockers };
|
|
1656
|
+
};
|
|
1657
|
+
var exploreToPlan = (s) => {
|
|
1658
|
+
const blockers = [];
|
|
1659
|
+
const parity = stageParity(s, "explore");
|
|
1660
|
+
if (parity) blockers.push(parity);
|
|
1661
|
+
const connKeys = Object.keys(s.connections ?? {});
|
|
1662
|
+
if (connKeys.length === 0) {
|
|
1663
|
+
blockers.push(
|
|
1664
|
+
"connections map is empty \u2014 run the explore-stage probe before advancing"
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
return { pass: blockers.length === 0, blockers };
|
|
1668
|
+
};
|
|
1669
|
+
var planToDesign = (s) => {
|
|
1670
|
+
const blockers = [];
|
|
1671
|
+
const parity = stageParity(s, "plan");
|
|
1672
|
+
if (parity) blockers.push(parity);
|
|
1673
|
+
if ((s.must_haves ?? []).length === 0) {
|
|
1674
|
+
blockers.push("must_haves is empty \u2014 discover stage must land at least M-01");
|
|
1675
|
+
}
|
|
1676
|
+
const hasLocked = (s.decisions ?? []).some((d) => d.status === "locked");
|
|
1677
|
+
if (!hasLocked) {
|
|
1678
|
+
blockers.push("no locked decision in <decisions> \u2014 at least one must be locked");
|
|
1679
|
+
}
|
|
1680
|
+
const failing = (s.must_haves ?? []).filter((m) => m.status === "fail");
|
|
1681
|
+
for (const mh of failing) {
|
|
1682
|
+
const reconciled = (s.decisions ?? []).some((d) => d.text.includes(mh.id));
|
|
1683
|
+
if (!reconciled) {
|
|
1684
|
+
blockers.push(`${mh.id} marked fail without a decision to reconcile`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return { pass: blockers.length === 0, blockers };
|
|
1688
|
+
};
|
|
1689
|
+
var designToVerify = (s) => {
|
|
1690
|
+
const blockers = [];
|
|
1691
|
+
const parity = stageParity(s, "design");
|
|
1692
|
+
if (parity) blockers.push(parity);
|
|
1693
|
+
const pending = (s.must_haves ?? []).filter((m) => m.status === "pending");
|
|
1694
|
+
for (const mh of pending) {
|
|
1695
|
+
const lower = mh.text.toLowerCase();
|
|
1696
|
+
const hit = DESIGN_KEYWORDS.find((kw) => lower.includes(kw));
|
|
1697
|
+
if (hit) {
|
|
1698
|
+
blockers.push(
|
|
1699
|
+
`${mh.id} is pending and mentions "${hit}" \u2014 resolve in design before verify`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return { pass: blockers.length === 0, blockers };
|
|
1704
|
+
};
|
|
1705
|
+
var GATES = Object.freeze({
|
|
1706
|
+
briefToExplore,
|
|
1707
|
+
exploreToPlan,
|
|
1708
|
+
planToDesign,
|
|
1709
|
+
designToVerify
|
|
1710
|
+
});
|
|
1711
|
+
function gateFor(from, to) {
|
|
1712
|
+
if (from === "brief" && to === "explore") return briefToExplore;
|
|
1713
|
+
if (from === "explore" && to === "plan") return exploreToPlan;
|
|
1714
|
+
if (from === "plan" && to === "design") return planToDesign;
|
|
1715
|
+
if (from === "design" && to === "verify") return designToVerify;
|
|
1716
|
+
return null;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// sdk/state/index.ts
|
|
1720
|
+
async function read(path) {
|
|
1721
|
+
const raw = (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
1722
|
+
return parse(raw).state;
|
|
1723
|
+
}
|
|
1724
|
+
async function mutate(path, fn) {
|
|
1725
|
+
const release = await acquire(path);
|
|
1726
|
+
const tmpPath = `${path}.tmp`;
|
|
1727
|
+
try {
|
|
1728
|
+
const raw = (0, import_node_fs2.readFileSync)(path, "utf8");
|
|
1729
|
+
const { state, raw_bodies, raw_frontmatter, block_gaps, line_ending } = parse(raw);
|
|
1730
|
+
const clone = structuredClone(state);
|
|
1731
|
+
const next = fn(clone);
|
|
1732
|
+
const out = serialize(next, {
|
|
1733
|
+
raw_frontmatter,
|
|
1734
|
+
raw_bodies,
|
|
1735
|
+
block_gaps,
|
|
1736
|
+
line_ending
|
|
1737
|
+
});
|
|
1738
|
+
(0, import_node_fs2.writeFileSync)(tmpPath, out, "utf8");
|
|
1739
|
+
try {
|
|
1740
|
+
(0, import_node_fs2.renameSync)(tmpPath, path);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
1743
|
+
if (code === "EPERM" || code === "EBUSY") {
|
|
1744
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1745
|
+
(0, import_node_fs2.renameSync)(tmpPath, path);
|
|
1746
|
+
} else {
|
|
1747
|
+
throw err;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return next;
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
try {
|
|
1753
|
+
if ((0, import_node_fs2.existsSync)(tmpPath)) (0, import_node_fs2.unlinkSync)(tmpPath);
|
|
1754
|
+
} catch {
|
|
1755
|
+
}
|
|
1756
|
+
throw err;
|
|
1757
|
+
} finally {
|
|
1758
|
+
await release();
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
async function transition(path, toStage) {
|
|
1762
|
+
const beforeMutate = await read(path);
|
|
1763
|
+
const from = beforeMutate.position.stage;
|
|
1764
|
+
if (!isStage(from)) {
|
|
1765
|
+
throw new TransitionGateFailed(toStage, [
|
|
1766
|
+
`Invalid transition: from="${from}" is not a recognized Stage`
|
|
1767
|
+
]);
|
|
1768
|
+
}
|
|
1769
|
+
const gate = gateFor(from, toStage);
|
|
1770
|
+
if (gate === null) {
|
|
1771
|
+
throw new TransitionGateFailed(toStage, [
|
|
1772
|
+
`Invalid transition: ${from} \u2192 ${toStage}`
|
|
1773
|
+
]);
|
|
1774
|
+
}
|
|
1775
|
+
const gateResult = gate(beforeMutate);
|
|
1776
|
+
if (!gateResult.pass) {
|
|
1777
|
+
throw new TransitionGateFailed(toStage, gateResult.blockers);
|
|
1778
|
+
}
|
|
1779
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1780
|
+
const nextState = await mutate(path, (s) => {
|
|
1781
|
+
s.frontmatter.stage = toStage;
|
|
1782
|
+
s.frontmatter.last_checkpoint = nowIso;
|
|
1783
|
+
s.position.stage = toStage;
|
|
1784
|
+
s.timestamps[`${toStage}_started_at`] = nowIso;
|
|
1785
|
+
return s;
|
|
1786
|
+
});
|
|
1787
|
+
return { pass: true, blockers: gateResult.blockers, state: nextState };
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// sdk/event-stream/index.ts
|
|
1791
|
+
var import_node_os2 = require("node:os");
|
|
1792
|
+
|
|
1793
|
+
// sdk/event-stream/emitter.ts
|
|
1794
|
+
var import_node_events = require("node:events");
|
|
1795
|
+
var DEFAULT_MAX_LISTENERS = 50;
|
|
1796
|
+
var EventBus = class extends import_node_events.EventEmitter {
|
|
1797
|
+
constructor() {
|
|
1798
|
+
super();
|
|
1799
|
+
this.setMaxListeners(DEFAULT_MAX_LISTENERS);
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Subscribe to one specific event type. The handler fires for every
|
|
1803
|
+
* subsequent `emit(type, ev)` call where `ev.type === type`. Returns
|
|
1804
|
+
* a closure that detaches the listener on invocation.
|
|
1805
|
+
*
|
|
1806
|
+
* @example
|
|
1807
|
+
* const off = bus.subscribe<StateMutationEvent>('state.mutation', (ev) => {
|
|
1808
|
+
* console.log(ev.payload.tool);
|
|
1809
|
+
* });
|
|
1810
|
+
* // …later
|
|
1811
|
+
* off();
|
|
1812
|
+
*/
|
|
1813
|
+
subscribe(type, handler) {
|
|
1814
|
+
const listener = handler;
|
|
1815
|
+
this.on(type, listener);
|
|
1816
|
+
return () => {
|
|
1817
|
+
this.off(type, listener);
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Subscribe to *every* event regardless of type. Listeners registered
|
|
1822
|
+
* here fire on the special `'*'` channel, which `appendEvent()`
|
|
1823
|
+
* re-emits to on every event. Returns an unsubscribe closure.
|
|
1824
|
+
*/
|
|
1825
|
+
subscribeAll(handler) {
|
|
1826
|
+
const listener = handler;
|
|
1827
|
+
this.on("*", listener);
|
|
1828
|
+
return () => {
|
|
1829
|
+
this.off("*", listener);
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// sdk/event-stream/writer.ts
|
|
1835
|
+
var import_node_fs3 = require("node:fs");
|
|
1836
|
+
var import_node_path = require("node:path");
|
|
1837
|
+
var import_node_module = require("node:module");
|
|
1838
|
+
function _findRepoRoot() {
|
|
1839
|
+
let dir = process.cwd();
|
|
1840
|
+
for (let i = 0; i < 8; i++) {
|
|
1841
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
1842
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
1843
|
+
if (parent === dir) break;
|
|
1844
|
+
dir = parent;
|
|
1845
|
+
}
|
|
1846
|
+
return process.cwd();
|
|
1847
|
+
}
|
|
1848
|
+
var _redact;
|
|
1849
|
+
try {
|
|
1850
|
+
const _root = _findRepoRoot();
|
|
1851
|
+
const _candidate = (0, import_node_path.resolve)(_root, "scripts/lib/redact.cjs");
|
|
1852
|
+
if ((0, import_node_fs3.existsSync)(_candidate)) {
|
|
1853
|
+
const _redactRequire = (0, import_node_module.createRequire)((0, import_node_path.join)(_root, "package.json"));
|
|
1854
|
+
const _mod = _redactRequire(_candidate);
|
|
1855
|
+
_redact = _mod.redact;
|
|
1856
|
+
} else {
|
|
1857
|
+
const _altRoot = (0, import_node_path.resolve)(_root, "..", "..");
|
|
1858
|
+
const _altCandidate = (0, import_node_path.resolve)(_altRoot, "scripts/lib/redact.cjs");
|
|
1859
|
+
if ((0, import_node_fs3.existsSync)(_altCandidate)) {
|
|
1860
|
+
const _altRequire = (0, import_node_module.createRequire)((0, import_node_path.join)(_altRoot, "package.json"));
|
|
1861
|
+
const _altMod = _altRequire(_altCandidate);
|
|
1862
|
+
_redact = _altMod.redact;
|
|
1863
|
+
} else {
|
|
1864
|
+
_redact = (v) => v;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
} catch {
|
|
1868
|
+
_redact = (v) => v;
|
|
1869
|
+
}
|
|
1870
|
+
var redact = _redact;
|
|
1871
|
+
var DEFAULT_EVENTS_PATH = ".design/telemetry/events.jsonl";
|
|
1872
|
+
var DEFAULT_MAX_LINE_BYTES = 64 * 1024;
|
|
1873
|
+
var EventWriter = class {
|
|
1874
|
+
/** Resolved absolute target path. */
|
|
1875
|
+
path;
|
|
1876
|
+
/** Maximum line size in bytes (see {@link WriterOptions.maxLineBytes}). */
|
|
1877
|
+
maxLineBytes;
|
|
1878
|
+
/** Number of failed append attempts since construction. */
|
|
1879
|
+
writeErrors = 0;
|
|
1880
|
+
/** The most recent write error, or `null` if none has occurred. */
|
|
1881
|
+
lastError = null;
|
|
1882
|
+
/** `true` once we've ensured the target directory exists. */
|
|
1883
|
+
directoryEnsured = false;
|
|
1884
|
+
constructor(opts = {}) {
|
|
1885
|
+
const rawPath = opts.path ?? DEFAULT_EVENTS_PATH;
|
|
1886
|
+
this.path = (0, import_node_path.isAbsolute)(rawPath) ? rawPath : (0, import_node_path.resolve)(process.cwd(), rawPath);
|
|
1887
|
+
this.maxLineBytes = opts.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Append one event to the target file.
|
|
1891
|
+
*
|
|
1892
|
+
* Contract:
|
|
1893
|
+
* * SYNC — returns when the write has been accepted by the kernel.
|
|
1894
|
+
* * NEVER throws — I/O errors increment {@link writeErrors} and update
|
|
1895
|
+
* {@link lastError}; a diagnostic is written to stderr and
|
|
1896
|
+
* execution continues.
|
|
1897
|
+
* * Truncates oversized payloads rather than dropping the event.
|
|
1898
|
+
*/
|
|
1899
|
+
append(ev) {
|
|
1900
|
+
try {
|
|
1901
|
+
const line = this.serialize(ev);
|
|
1902
|
+
this.ensureDirectory();
|
|
1903
|
+
(0, import_node_fs3.appendFileSync)(this.path, line, { flag: "a" });
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
this.writeErrors += 1;
|
|
1906
|
+
this.lastError = err instanceof Error ? err : new Error(String(err));
|
|
1907
|
+
try {
|
|
1908
|
+
process.stderr.write(
|
|
1909
|
+
`[event-stream] write failed: ${this.lastError.message}
|
|
1910
|
+
`
|
|
1911
|
+
);
|
|
1912
|
+
} catch {
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Produce the on-disk JSONL representation of an event, truncating
|
|
1918
|
+
* oversized payloads so the line fits within {@link maxLineBytes}.
|
|
1919
|
+
*
|
|
1920
|
+
* Exposed on the instance for unit-testability; callers should use
|
|
1921
|
+
* {@link append}.
|
|
1922
|
+
*/
|
|
1923
|
+
serialize(ev) {
|
|
1924
|
+
const scrubbed = redact(ev);
|
|
1925
|
+
const raw = JSON.stringify(scrubbed) + "\n";
|
|
1926
|
+
if (Buffer.byteLength(raw, "utf8") <= this.maxLineBytes) {
|
|
1927
|
+
return raw;
|
|
1928
|
+
}
|
|
1929
|
+
const truncated = {
|
|
1930
|
+
type: scrubbed.type,
|
|
1931
|
+
timestamp: scrubbed.timestamp,
|
|
1932
|
+
sessionId: scrubbed.sessionId,
|
|
1933
|
+
payload: { _truncated_placeholder: true },
|
|
1934
|
+
_truncated: true
|
|
1935
|
+
};
|
|
1936
|
+
if (scrubbed.stage !== void 0) truncated.stage = scrubbed.stage;
|
|
1937
|
+
if (scrubbed.cycle !== void 0) truncated.cycle = scrubbed.cycle;
|
|
1938
|
+
if (scrubbed._meta !== void 0) truncated._meta = scrubbed._meta;
|
|
1939
|
+
return JSON.stringify(truncated) + "\n";
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Ensure the target directory exists. Memoized so we only pay the
|
|
1943
|
+
* filesystem stat cost once per writer lifetime.
|
|
1944
|
+
*/
|
|
1945
|
+
ensureDirectory() {
|
|
1946
|
+
if (this.directoryEnsured) return;
|
|
1947
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path.dirname)(this.path), { recursive: true });
|
|
1948
|
+
this.directoryEnsured = true;
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
// sdk/event-stream/index.ts
|
|
1953
|
+
var defaultWriter = null;
|
|
1954
|
+
var defaultBus = null;
|
|
1955
|
+
var cachedHost = null;
|
|
1956
|
+
function getWriter(opts) {
|
|
1957
|
+
if (defaultWriter === null) {
|
|
1958
|
+
const envPath = process.env["GDD_EVENTS_PATH"];
|
|
1959
|
+
const finalOpts = opts?.path === void 0 && envPath !== void 0 && envPath.length > 0 ? { ...opts ?? {}, path: envPath } : opts ?? {};
|
|
1960
|
+
defaultWriter = new EventWriter(finalOpts);
|
|
1961
|
+
}
|
|
1962
|
+
return defaultWriter;
|
|
1963
|
+
}
|
|
1964
|
+
function getBus() {
|
|
1965
|
+
if (defaultBus === null) {
|
|
1966
|
+
defaultBus = new EventBus();
|
|
1967
|
+
}
|
|
1968
|
+
return defaultBus;
|
|
1969
|
+
}
|
|
1970
|
+
function appendEvent(ev) {
|
|
1971
|
+
if (ev._meta === void 0) {
|
|
1972
|
+
if (cachedHost === null) {
|
|
1973
|
+
try {
|
|
1974
|
+
cachedHost = (0, import_node_os2.hostname)();
|
|
1975
|
+
} catch {
|
|
1976
|
+
cachedHost = "unknown";
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
const meta = {
|
|
1980
|
+
pid: process.pid,
|
|
1981
|
+
host: cachedHost,
|
|
1982
|
+
source: "event-stream"
|
|
1983
|
+
};
|
|
1984
|
+
ev._meta = meta;
|
|
1985
|
+
}
|
|
1986
|
+
getWriter().append(ev);
|
|
1987
|
+
const bus = getBus();
|
|
1988
|
+
bus.emit(ev.type, ev);
|
|
1989
|
+
bus.emit("*", ev);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// sdk/mcp/gdd-state/tools/shared.ts
|
|
1993
|
+
function makeSessionId() {
|
|
1994
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1995
|
+
return `gdd-mcp-${iso}-${process.pid}`;
|
|
1996
|
+
}
|
|
1997
|
+
var CACHED_SESSION_ID = null;
|
|
1998
|
+
function getSessionId() {
|
|
1999
|
+
if (CACHED_SESSION_ID === null) CACHED_SESSION_ID = makeSessionId();
|
|
2000
|
+
return CACHED_SESSION_ID;
|
|
2001
|
+
}
|
|
2002
|
+
function resolveStatePath() {
|
|
2003
|
+
const override = process.env["GDD_STATE_PATH"];
|
|
2004
|
+
if (typeof override === "string" && override.length > 0) return override;
|
|
2005
|
+
return ".design/STATE.md";
|
|
2006
|
+
}
|
|
2007
|
+
function isStageValue(value) {
|
|
2008
|
+
return value === "brief" || value === "explore" || value === "plan" || value === "design" || value === "verify";
|
|
2009
|
+
}
|
|
2010
|
+
function emitStateMutation(tool, diff, stateAfter) {
|
|
2011
|
+
const stage = isStageValue(stateAfter.position.stage) ? stateAfter.position.stage : void 0;
|
|
2012
|
+
const ev = {
|
|
2013
|
+
type: "state.mutation",
|
|
2014
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2015
|
+
sessionId: getSessionId(),
|
|
2016
|
+
...stage !== void 0 ? { stage } : {},
|
|
2017
|
+
...typeof stateAfter.frontmatter.cycle === "string" && stateAfter.frontmatter.cycle.length > 0 ? { cycle: stateAfter.frontmatter.cycle } : {},
|
|
2018
|
+
payload: { tool, diff }
|
|
2019
|
+
};
|
|
2020
|
+
appendEvent(ev);
|
|
2021
|
+
}
|
|
2022
|
+
function emitStateTransition(from, to, pass, blockers, state) {
|
|
2023
|
+
const cycle = state !== null && typeof state.frontmatter.cycle === "string" && state.frontmatter.cycle.length > 0 ? state.frontmatter.cycle : void 0;
|
|
2024
|
+
const ev = {
|
|
2025
|
+
type: "state.transition",
|
|
2026
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2027
|
+
sessionId: getSessionId(),
|
|
2028
|
+
stage: pass ? to : from,
|
|
2029
|
+
...cycle !== void 0 ? { cycle } : {},
|
|
2030
|
+
payload: { from, to, blockers: [...blockers], pass }
|
|
2031
|
+
};
|
|
2032
|
+
appendEvent(ev);
|
|
2033
|
+
}
|
|
2034
|
+
function errorResponse(err) {
|
|
2035
|
+
const payload = toToolError(err);
|
|
2036
|
+
return { success: false, error: payload.error };
|
|
2037
|
+
}
|
|
2038
|
+
function okResponse(data) {
|
|
2039
|
+
return { success: true, data };
|
|
2040
|
+
}
|
|
2041
|
+
function throwValidation(codeSuffix, message, context) {
|
|
2042
|
+
throw new ValidationError(message, `VALIDATION_${codeSuffix}`, context);
|
|
2043
|
+
}
|
|
2044
|
+
function operationFailed(codeSuffix, message, context) {
|
|
2045
|
+
throw new OperationFailedError(
|
|
2046
|
+
message,
|
|
2047
|
+
`OPERATION_${codeSuffix}`,
|
|
2048
|
+
context
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// sdk/mcp/gdd-state/tools/get.ts
|
|
2053
|
+
var name = "gdd_state__get";
|
|
2054
|
+
var schemaPath = "../schemas/get.schema.json";
|
|
2055
|
+
function project(state, fields) {
|
|
2056
|
+
const allowed = new Set(fields);
|
|
2057
|
+
const out = {};
|
|
2058
|
+
for (const [k, v] of Object.entries(state)) {
|
|
2059
|
+
if (allowed.has(k)) out[k] = v;
|
|
2060
|
+
}
|
|
2061
|
+
return out;
|
|
2062
|
+
}
|
|
2063
|
+
async function handle(input) {
|
|
2064
|
+
try {
|
|
2065
|
+
const typed = input ?? {};
|
|
2066
|
+
const path = resolveStatePath();
|
|
2067
|
+
const state = await read(path);
|
|
2068
|
+
const fields = Array.isArray(typed.fields) ? typed.fields : null;
|
|
2069
|
+
const stateOut = fields === null || fields.length === 0 ? state : project(state, fields);
|
|
2070
|
+
return okResponse({ state: stateOut, path });
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
return errorResponse(err);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// sdk/mcp/gdd-state/tools/update_progress.ts
|
|
2077
|
+
var update_progress_exports = {};
|
|
2078
|
+
__export(update_progress_exports, {
|
|
2079
|
+
handle: () => handle2,
|
|
2080
|
+
name: () => name2,
|
|
2081
|
+
schemaPath: () => schemaPath2
|
|
2082
|
+
});
|
|
2083
|
+
var name2 = "gdd_state__update_progress";
|
|
2084
|
+
var schemaPath2 = "../schemas/update_progress.schema.json";
|
|
2085
|
+
var TASK_PROGRESS_RE = /^[0-9]+\/[0-9]+$/;
|
|
2086
|
+
var STATUSES = /* @__PURE__ */ new Set([
|
|
2087
|
+
"initialized",
|
|
2088
|
+
"in_progress",
|
|
2089
|
+
"completed",
|
|
2090
|
+
"blocked"
|
|
2091
|
+
]);
|
|
2092
|
+
async function handle2(input) {
|
|
2093
|
+
try {
|
|
2094
|
+
const typed = input ?? {};
|
|
2095
|
+
if (typed.task_progress === void 0 && typed.status === void 0) {
|
|
2096
|
+
throwValidation(
|
|
2097
|
+
"MISSING_FIELD",
|
|
2098
|
+
"update_progress requires at least one of task_progress / status"
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
if (typed.task_progress !== void 0) {
|
|
2102
|
+
if (!TASK_PROGRESS_RE.test(typed.task_progress)) {
|
|
2103
|
+
throwValidation(
|
|
2104
|
+
"TASK_PROGRESS_FORMAT",
|
|
2105
|
+
`task_progress "${typed.task_progress}" must match N/M (digits)`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (typed.status !== void 0 && !STATUSES.has(typed.status)) {
|
|
2110
|
+
throwValidation(
|
|
2111
|
+
"STATUS_INVALID",
|
|
2112
|
+
`status "${typed.status}" is not one of initialized/in_progress/completed/blocked`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
const path = resolveStatePath();
|
|
2116
|
+
const diff = {};
|
|
2117
|
+
const after = await mutate(path, (s) => {
|
|
2118
|
+
if (typed.task_progress !== void 0) {
|
|
2119
|
+
diff["task_progress"] = {
|
|
2120
|
+
before: s.position.task_progress,
|
|
2121
|
+
after: typed.task_progress
|
|
2122
|
+
};
|
|
2123
|
+
s.position.task_progress = typed.task_progress;
|
|
2124
|
+
}
|
|
2125
|
+
if (typed.status !== void 0) {
|
|
2126
|
+
diff["status"] = { before: s.position.status, after: typed.status };
|
|
2127
|
+
s.position.status = typed.status;
|
|
2128
|
+
}
|
|
2129
|
+
return s;
|
|
2130
|
+
});
|
|
2131
|
+
emitStateMutation(name2, diff, after);
|
|
2132
|
+
return okResponse({ position: after.position });
|
|
2133
|
+
} catch (err) {
|
|
2134
|
+
return errorResponse(err);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// sdk/mcp/gdd-state/tools/transition_stage.ts
|
|
2139
|
+
var transition_stage_exports = {};
|
|
2140
|
+
__export(transition_stage_exports, {
|
|
2141
|
+
handle: () => handle3,
|
|
2142
|
+
name: () => name3,
|
|
2143
|
+
schemaPath: () => schemaPath3
|
|
2144
|
+
});
|
|
2145
|
+
var name3 = "gdd_state__transition_stage";
|
|
2146
|
+
var schemaPath3 = "../schemas/transition_stage.schema.json";
|
|
2147
|
+
async function handle3(input) {
|
|
2148
|
+
try {
|
|
2149
|
+
const typed = input ?? {};
|
|
2150
|
+
if (!isStage(typed.to)) {
|
|
2151
|
+
throwValidation(
|
|
2152
|
+
"STAGE_INVALID",
|
|
2153
|
+
`to "${String(typed.to)}" is not a recognized Stage`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
const path = resolveStatePath();
|
|
2157
|
+
const before = await read(path);
|
|
2158
|
+
const fromValue = before.position.stage;
|
|
2159
|
+
try {
|
|
2160
|
+
const result = await transition(path, typed.to);
|
|
2161
|
+
const fromNarrow = isStage(fromValue) ? fromValue : typed.to;
|
|
2162
|
+
emitStateTransition(fromNarrow, typed.to, true, [], result.state);
|
|
2163
|
+
return okResponse({
|
|
2164
|
+
from: fromValue,
|
|
2165
|
+
to: typed.to,
|
|
2166
|
+
state: result.state
|
|
2167
|
+
});
|
|
2168
|
+
} catch (inner) {
|
|
2169
|
+
if (inner instanceof TransitionGateFailed) {
|
|
2170
|
+
const fromNarrow = isStage(fromValue) ? fromValue : typed.to;
|
|
2171
|
+
emitStateTransition(
|
|
2172
|
+
fromNarrow,
|
|
2173
|
+
typed.to,
|
|
2174
|
+
false,
|
|
2175
|
+
[...inner.blockers],
|
|
2176
|
+
before
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
throw inner;
|
|
2180
|
+
}
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
return errorResponse(err);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// sdk/mcp/gdd-state/tools/add_blocker.ts
|
|
2187
|
+
var add_blocker_exports = {};
|
|
2188
|
+
__export(add_blocker_exports, {
|
|
2189
|
+
handle: () => handle4,
|
|
2190
|
+
name: () => name4,
|
|
2191
|
+
schemaPath: () => schemaPath4
|
|
2192
|
+
});
|
|
2193
|
+
var name4 = "gdd_state__add_blocker";
|
|
2194
|
+
var schemaPath4 = "../schemas/add_blocker.schema.json";
|
|
2195
|
+
var DATE_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
2196
|
+
function today() {
|
|
2197
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2198
|
+
}
|
|
2199
|
+
async function handle4(input) {
|
|
2200
|
+
try {
|
|
2201
|
+
const typed = input ?? {};
|
|
2202
|
+
if (typeof typed.text !== "string" || typed.text.length === 0) {
|
|
2203
|
+
throwValidation("MISSING_FIELD", "add_blocker requires a non-empty text");
|
|
2204
|
+
}
|
|
2205
|
+
if (typed.date !== void 0 && !DATE_RE.test(typed.date)) {
|
|
2206
|
+
throwValidation(
|
|
2207
|
+
"DATE_FORMAT",
|
|
2208
|
+
`date "${typed.date}" must be YYYY-MM-DD`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
const path = resolveStatePath();
|
|
2212
|
+
let appended = null;
|
|
2213
|
+
let countAfter = 0;
|
|
2214
|
+
const after = await mutate(path, (s) => {
|
|
2215
|
+
const blocker = {
|
|
2216
|
+
stage: typed.stage ?? s.position.stage ?? "",
|
|
2217
|
+
date: typed.date ?? today(),
|
|
2218
|
+
text: typed.text
|
|
2219
|
+
};
|
|
2220
|
+
s.blockers.push(blocker);
|
|
2221
|
+
appended = blocker;
|
|
2222
|
+
countAfter = s.blockers.length;
|
|
2223
|
+
return s;
|
|
2224
|
+
});
|
|
2225
|
+
if (appended === null) {
|
|
2226
|
+
throwValidation("INTERNAL", "blocker was not appended (unreachable)");
|
|
2227
|
+
}
|
|
2228
|
+
emitStateMutation(name4, { appended, count: countAfter }, after);
|
|
2229
|
+
return okResponse({ blocker: appended, count: countAfter });
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
return errorResponse(err);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// sdk/mcp/gdd-state/tools/resolve_blocker.ts
|
|
2236
|
+
var resolve_blocker_exports = {};
|
|
2237
|
+
__export(resolve_blocker_exports, {
|
|
2238
|
+
handle: () => handle5,
|
|
2239
|
+
name: () => name5,
|
|
2240
|
+
schemaPath: () => schemaPath5
|
|
2241
|
+
});
|
|
2242
|
+
var name5 = "gdd_state__resolve_blocker";
|
|
2243
|
+
var schemaPath5 = "../schemas/resolve_blocker.schema.json";
|
|
2244
|
+
async function handle5(input) {
|
|
2245
|
+
try {
|
|
2246
|
+
const typed = input ?? {};
|
|
2247
|
+
const hasIndex = typeof typed.index === "number";
|
|
2248
|
+
const hasText = typeof typed.text === "string" && typed.text.length > 0;
|
|
2249
|
+
if (hasIndex === hasText) {
|
|
2250
|
+
throwValidation(
|
|
2251
|
+
"ONEOF_REQUIRED",
|
|
2252
|
+
"resolve_blocker requires exactly one of: index OR text"
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
if (hasIndex && typed.index < 0) {
|
|
2256
|
+
throwValidation("INDEX_NEGATIVE", "index must be >= 0");
|
|
2257
|
+
}
|
|
2258
|
+
const path = resolveStatePath();
|
|
2259
|
+
let removed = null;
|
|
2260
|
+
let countAfter = 0;
|
|
2261
|
+
const after = await mutate(path, (s) => {
|
|
2262
|
+
if (hasIndex) {
|
|
2263
|
+
const idx = typed.index;
|
|
2264
|
+
if (idx >= s.blockers.length) {
|
|
2265
|
+
operationFailed(
|
|
2266
|
+
"BLOCKER_NOT_FOUND",
|
|
2267
|
+
`no blocker at index ${idx} (length=${s.blockers.length})`,
|
|
2268
|
+
{ index: idx, length: s.blockers.length }
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
const [deleted] = s.blockers.splice(idx, 1);
|
|
2272
|
+
removed = deleted ?? null;
|
|
2273
|
+
} else {
|
|
2274
|
+
const target = typed.text;
|
|
2275
|
+
const idx = s.blockers.findIndex((b) => b.text === target);
|
|
2276
|
+
if (idx === -1) {
|
|
2277
|
+
operationFailed(
|
|
2278
|
+
"BLOCKER_NOT_FOUND",
|
|
2279
|
+
`no blocker matches text "${target}"`,
|
|
2280
|
+
{ text: target }
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
const [deleted] = s.blockers.splice(idx, 1);
|
|
2284
|
+
removed = deleted ?? null;
|
|
2285
|
+
}
|
|
2286
|
+
countAfter = s.blockers.length;
|
|
2287
|
+
return s;
|
|
2288
|
+
});
|
|
2289
|
+
if (removed === null) {
|
|
2290
|
+
operationFailed("BLOCKER_NOT_FOUND", "no blocker was removed (unreachable)");
|
|
2291
|
+
}
|
|
2292
|
+
emitStateMutation(name5, { removed, count: countAfter }, after);
|
|
2293
|
+
return okResponse({ removed, count: countAfter });
|
|
2294
|
+
} catch (err) {
|
|
2295
|
+
return errorResponse(err);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// sdk/mcp/gdd-state/tools/add_decision.ts
|
|
2300
|
+
var add_decision_exports = {};
|
|
2301
|
+
__export(add_decision_exports, {
|
|
2302
|
+
handle: () => handle6,
|
|
2303
|
+
name: () => name6,
|
|
2304
|
+
schemaPath: () => schemaPath6
|
|
2305
|
+
});
|
|
2306
|
+
var name6 = "gdd_state__add_decision";
|
|
2307
|
+
var schemaPath6 = "../schemas/add_decision.schema.json";
|
|
2308
|
+
var ID_RE = /^D-([0-9]+)$/;
|
|
2309
|
+
function nextDecisionId(existing) {
|
|
2310
|
+
let max = 0;
|
|
2311
|
+
for (const d of existing) {
|
|
2312
|
+
const m = d.id.match(ID_RE);
|
|
2313
|
+
if (m && m[1] !== void 0) {
|
|
2314
|
+
const n = Number.parseInt(m[1], 10);
|
|
2315
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return `D-${max + 1}`;
|
|
2319
|
+
}
|
|
2320
|
+
async function handle6(input) {
|
|
2321
|
+
try {
|
|
2322
|
+
const typed = input ?? {};
|
|
2323
|
+
if (typeof typed.text !== "string" || typed.text.length === 0) {
|
|
2324
|
+
throwValidation("MISSING_FIELD", "add_decision requires a non-empty text");
|
|
2325
|
+
}
|
|
2326
|
+
if (typed.status !== void 0 && typed.status !== "locked" && typed.status !== "tentative") {
|
|
2327
|
+
throwValidation(
|
|
2328
|
+
"STATUS_INVALID",
|
|
2329
|
+
`status "${String(typed.status)}" is not one of locked/tentative`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
if (typed.id !== void 0 && !ID_RE.test(typed.id)) {
|
|
2333
|
+
throwValidation("ID_FORMAT", `id "${typed.id}" must match D-<digits>`);
|
|
2334
|
+
}
|
|
2335
|
+
const path = resolveStatePath();
|
|
2336
|
+
let appended = null;
|
|
2337
|
+
let countAfter = 0;
|
|
2338
|
+
const after = await mutate(path, (s) => {
|
|
2339
|
+
const decision = {
|
|
2340
|
+
id: typed.id ?? nextDecisionId(s.decisions),
|
|
2341
|
+
text: typed.text,
|
|
2342
|
+
status: typed.status ?? "tentative"
|
|
2343
|
+
};
|
|
2344
|
+
s.decisions.push(decision);
|
|
2345
|
+
appended = decision;
|
|
2346
|
+
countAfter = s.decisions.length;
|
|
2347
|
+
return s;
|
|
2348
|
+
});
|
|
2349
|
+
if (appended === null) {
|
|
2350
|
+
throwValidation("INTERNAL", "decision was not appended (unreachable)");
|
|
2351
|
+
}
|
|
2352
|
+
emitStateMutation(name6, { appended, count: countAfter }, after);
|
|
2353
|
+
return okResponse({ decision: appended, count: countAfter });
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
return errorResponse(err);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// sdk/mcp/gdd-state/tools/add_must_have.ts
|
|
2360
|
+
var add_must_have_exports = {};
|
|
2361
|
+
__export(add_must_have_exports, {
|
|
2362
|
+
handle: () => handle7,
|
|
2363
|
+
name: () => name7,
|
|
2364
|
+
schemaPath: () => schemaPath7
|
|
2365
|
+
});
|
|
2366
|
+
var name7 = "gdd_state__add_must_have";
|
|
2367
|
+
var schemaPath7 = "../schemas/add_must_have.schema.json";
|
|
2368
|
+
var ID_RE2 = /^M-([0-9]+)$/;
|
|
2369
|
+
function nextMustHaveId(existing) {
|
|
2370
|
+
let max = 0;
|
|
2371
|
+
for (const m of existing) {
|
|
2372
|
+
const match = m.id.match(ID_RE2);
|
|
2373
|
+
if (match && match[1] !== void 0) {
|
|
2374
|
+
const n = Number.parseInt(match[1], 10);
|
|
2375
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return `M-${max + 1}`;
|
|
2379
|
+
}
|
|
2380
|
+
async function handle7(input) {
|
|
2381
|
+
try {
|
|
2382
|
+
const typed = input ?? {};
|
|
2383
|
+
if (typeof typed.text !== "string" || typed.text.length === 0) {
|
|
2384
|
+
throwValidation(
|
|
2385
|
+
"MISSING_FIELD",
|
|
2386
|
+
"add_must_have requires a non-empty text"
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
if (typed.status !== void 0 && typed.status !== "pending" && typed.status !== "pass" && typed.status !== "fail") {
|
|
2390
|
+
throwValidation(
|
|
2391
|
+
"STATUS_INVALID",
|
|
2392
|
+
`status "${String(typed.status)}" is not one of pending/pass/fail`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
if (typed.id !== void 0 && !ID_RE2.test(typed.id)) {
|
|
2396
|
+
throwValidation("ID_FORMAT", `id "${typed.id}" must match M-<digits>`);
|
|
2397
|
+
}
|
|
2398
|
+
const path = resolveStatePath();
|
|
2399
|
+
let written = null;
|
|
2400
|
+
let action = "insert";
|
|
2401
|
+
let countAfter = 0;
|
|
2402
|
+
const after = await mutate(path, (s) => {
|
|
2403
|
+
const mh = {
|
|
2404
|
+
id: typed.id ?? nextMustHaveId(s.must_haves),
|
|
2405
|
+
text: typed.text,
|
|
2406
|
+
status: typed.status ?? "pending"
|
|
2407
|
+
};
|
|
2408
|
+
const existingIdx = typed.id !== void 0 ? s.must_haves.findIndex((existing) => existing.id === mh.id) : -1;
|
|
2409
|
+
if (existingIdx >= 0) {
|
|
2410
|
+
s.must_haves[existingIdx] = mh;
|
|
2411
|
+
action = "update";
|
|
2412
|
+
} else {
|
|
2413
|
+
s.must_haves.push(mh);
|
|
2414
|
+
action = "insert";
|
|
2415
|
+
}
|
|
2416
|
+
written = mh;
|
|
2417
|
+
countAfter = s.must_haves.length;
|
|
2418
|
+
return s;
|
|
2419
|
+
});
|
|
2420
|
+
if (written === null) {
|
|
2421
|
+
throwValidation("INTERNAL", "must_have was not written (unreachable)");
|
|
2422
|
+
}
|
|
2423
|
+
emitStateMutation(
|
|
2424
|
+
name7,
|
|
2425
|
+
{ must_have: written, action, count: countAfter },
|
|
2426
|
+
after
|
|
2427
|
+
);
|
|
2428
|
+
return okResponse({ must_have: written, action, count: countAfter });
|
|
2429
|
+
} catch (err) {
|
|
2430
|
+
return errorResponse(err);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// sdk/mcp/gdd-state/tools/set_status.ts
|
|
2435
|
+
var set_status_exports = {};
|
|
2436
|
+
__export(set_status_exports, {
|
|
2437
|
+
handle: () => handle8,
|
|
2438
|
+
name: () => name8,
|
|
2439
|
+
schemaPath: () => schemaPath8
|
|
2440
|
+
});
|
|
2441
|
+
var name8 = "gdd_state__set_status";
|
|
2442
|
+
var schemaPath8 = "../schemas/set_status.schema.json";
|
|
2443
|
+
var STATUSES2 = /* @__PURE__ */ new Set([
|
|
2444
|
+
"initialized",
|
|
2445
|
+
"in_progress",
|
|
2446
|
+
"completed",
|
|
2447
|
+
"blocked"
|
|
2448
|
+
]);
|
|
2449
|
+
async function handle8(input) {
|
|
2450
|
+
try {
|
|
2451
|
+
const typed = input ?? {};
|
|
2452
|
+
if (typeof typed.status !== "string" || !STATUSES2.has(typed.status)) {
|
|
2453
|
+
throwValidation(
|
|
2454
|
+
"STATUS_INVALID",
|
|
2455
|
+
`status "${String(typed.status)}" is not one of initialized/in_progress/completed/blocked`
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
const path = resolveStatePath();
|
|
2459
|
+
const diff = {};
|
|
2460
|
+
const after = await mutate(path, (s) => {
|
|
2461
|
+
diff["status"] = { before: s.position.status, after: typed.status };
|
|
2462
|
+
s.position.status = typed.status;
|
|
2463
|
+
return s;
|
|
2464
|
+
});
|
|
2465
|
+
emitStateMutation(name8, diff, after);
|
|
2466
|
+
return okResponse({ status: after.position.status });
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
return errorResponse(err);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// sdk/mcp/gdd-state/tools/checkpoint.ts
|
|
2473
|
+
var checkpoint_exports = {};
|
|
2474
|
+
__export(checkpoint_exports, {
|
|
2475
|
+
handle: () => handle9,
|
|
2476
|
+
name: () => name9,
|
|
2477
|
+
schemaPath: () => schemaPath9
|
|
2478
|
+
});
|
|
2479
|
+
var name9 = "gdd_state__checkpoint";
|
|
2480
|
+
var schemaPath9 = "../schemas/checkpoint.schema.json";
|
|
2481
|
+
async function handle9(input) {
|
|
2482
|
+
try {
|
|
2483
|
+
const typed = input ?? {};
|
|
2484
|
+
if (typed.label !== void 0 && (typeof typed.label !== "string" || typed.label.length === 0)) {
|
|
2485
|
+
throwValidation("LABEL_FORMAT", "label must be a non-empty string");
|
|
2486
|
+
}
|
|
2487
|
+
const path = resolveStatePath();
|
|
2488
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2489
|
+
let timestampKey = "";
|
|
2490
|
+
const after = await mutate(path, (s) => {
|
|
2491
|
+
s.frontmatter.last_checkpoint = nowIso;
|
|
2492
|
+
const stage = typeof s.position.stage === "string" ? s.position.stage : "";
|
|
2493
|
+
timestampKey = typed.label !== void 0 ? `${typed.label}_at` : stage.length > 0 ? `${stage}_checkpoint_at` : "checkpoint_at";
|
|
2494
|
+
s.timestamps[timestampKey] = nowIso;
|
|
2495
|
+
return s;
|
|
2496
|
+
});
|
|
2497
|
+
emitStateMutation(
|
|
2498
|
+
name9,
|
|
2499
|
+
{ last_checkpoint: nowIso, timestamp_key: timestampKey },
|
|
2500
|
+
after
|
|
2501
|
+
);
|
|
2502
|
+
return okResponse({ last_checkpoint: nowIso, timestamp_key: timestampKey });
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
return errorResponse(err);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// sdk/mcp/gdd-state/tools/probe_connections.ts
|
|
2509
|
+
var probe_connections_exports = {};
|
|
2510
|
+
__export(probe_connections_exports, {
|
|
2511
|
+
handle: () => handle10,
|
|
2512
|
+
name: () => name10,
|
|
2513
|
+
schemaPath: () => schemaPath10
|
|
2514
|
+
});
|
|
2515
|
+
var name10 = "gdd_state__probe_connections";
|
|
2516
|
+
var schemaPath10 = "../schemas/probe_connections.schema.json";
|
|
2517
|
+
async function handle10(input) {
|
|
2518
|
+
try {
|
|
2519
|
+
const typed = input ?? {};
|
|
2520
|
+
if (!Array.isArray(typed.probe_results) || typed.probe_results.length === 0) {
|
|
2521
|
+
throwValidation(
|
|
2522
|
+
"MISSING_FIELD",
|
|
2523
|
+
"probe_connections requires a non-empty probe_results array"
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
for (const p of typed.probe_results) {
|
|
2527
|
+
if (!p || typeof p.name !== "string" || p.name.length === 0) {
|
|
2528
|
+
throwValidation(
|
|
2529
|
+
"PROBE_RESULT_NAME",
|
|
2530
|
+
"each probe_result must have a non-empty name"
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
if (!isConnectionStatus(p.status)) {
|
|
2534
|
+
throwValidation(
|
|
2535
|
+
"PROBE_RESULT_STATUS",
|
|
2536
|
+
`status "${String(p.status)}" is not one of available/unavailable/not_configured`
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
const path = resolveStatePath();
|
|
2541
|
+
const updated = [];
|
|
2542
|
+
const diff = {};
|
|
2543
|
+
const after = await mutate(path, (s) => {
|
|
2544
|
+
for (const p of typed.probe_results) {
|
|
2545
|
+
const before = Object.prototype.hasOwnProperty.call(s.connections, p.name) ? s.connections[p.name] : null;
|
|
2546
|
+
s.connections[p.name] = p.status;
|
|
2547
|
+
updated.push(p.name);
|
|
2548
|
+
diff[p.name] = { before, after: p.status };
|
|
2549
|
+
}
|
|
2550
|
+
return s;
|
|
2551
|
+
});
|
|
2552
|
+
emitStateMutation(name10, { diff }, after);
|
|
2553
|
+
return okResponse({ updated, connections: after.connections });
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
return errorResponse(err);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// sdk/mcp/gdd-state/tools/frontmatter_update.ts
|
|
2560
|
+
var frontmatter_update_exports = {};
|
|
2561
|
+
__export(frontmatter_update_exports, {
|
|
2562
|
+
FORBIDDEN_KEYS: () => FORBIDDEN_KEYS,
|
|
2563
|
+
handle: () => handle11,
|
|
2564
|
+
name: () => name11,
|
|
2565
|
+
schemaPath: () => schemaPath11
|
|
2566
|
+
});
|
|
2567
|
+
var name11 = "gdd_state__frontmatter_update";
|
|
2568
|
+
var schemaPath11 = "../schemas/frontmatter_update.schema.json";
|
|
2569
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
2570
|
+
"pipeline_state_version",
|
|
2571
|
+
"stage"
|
|
2572
|
+
]);
|
|
2573
|
+
function isScalar(v) {
|
|
2574
|
+
return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
2575
|
+
}
|
|
2576
|
+
async function handle11(input) {
|
|
2577
|
+
try {
|
|
2578
|
+
const typed = input ?? {};
|
|
2579
|
+
if (typed.patch === void 0 || typed.patch === null || typeof typed.patch !== "object") {
|
|
2580
|
+
throwValidation(
|
|
2581
|
+
"MISSING_FIELD",
|
|
2582
|
+
"frontmatter_update requires an object patch"
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const keys = Object.keys(typed.patch);
|
|
2586
|
+
if (keys.length === 0) {
|
|
2587
|
+
throwValidation("EMPTY_PATCH", "patch must contain at least one key");
|
|
2588
|
+
}
|
|
2589
|
+
for (const k of keys) {
|
|
2590
|
+
if (FORBIDDEN_KEYS.has(k)) {
|
|
2591
|
+
throwValidation(
|
|
2592
|
+
"FORBIDDEN_KEY",
|
|
2593
|
+
`patching "${k}" is not allowed via frontmatter_update (stage must go through transition_stage)`,
|
|
2594
|
+
{ key: k }
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
const value = typed.patch[k];
|
|
2598
|
+
if (!isScalar(value)) {
|
|
2599
|
+
throwValidation(
|
|
2600
|
+
"NON_SCALAR_VALUE",
|
|
2601
|
+
`patch value for "${k}" must be a string, number, or boolean`,
|
|
2602
|
+
{ key: k }
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const path = resolveStatePath();
|
|
2607
|
+
const diff = {};
|
|
2608
|
+
const after = await mutate(path, (s) => {
|
|
2609
|
+
for (const k of keys) {
|
|
2610
|
+
const before = s.frontmatter[k];
|
|
2611
|
+
const value = typed.patch[k];
|
|
2612
|
+
s.frontmatter[k] = value;
|
|
2613
|
+
diff[k] = { before, after: value };
|
|
2614
|
+
}
|
|
2615
|
+
return s;
|
|
2616
|
+
});
|
|
2617
|
+
emitStateMutation(name11, { diff }, after);
|
|
2618
|
+
return okResponse({ frontmatter: after.frontmatter, keys });
|
|
2619
|
+
} catch (err) {
|
|
2620
|
+
return errorResponse(err);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// sdk/mcp/gdd-state/tools/index.ts
|
|
2625
|
+
var TOOL_MODULES = [
|
|
2626
|
+
get_exports,
|
|
2627
|
+
add_blocker_exports,
|
|
2628
|
+
add_decision_exports,
|
|
2629
|
+
add_must_have_exports,
|
|
2630
|
+
checkpoint_exports,
|
|
2631
|
+
frontmatter_update_exports,
|
|
2632
|
+
probe_connections_exports,
|
|
2633
|
+
resolve_blocker_exports,
|
|
2634
|
+
set_status_exports,
|
|
2635
|
+
transition_stage_exports,
|
|
2636
|
+
update_progress_exports
|
|
2637
|
+
];
|
|
2638
|
+
var TOOL_COUNT = TOOL_MODULES.length;
|
|
2639
|
+
|
|
2640
|
+
// sdk/mcp/gdd-state/server.ts
|
|
2641
|
+
var SERVER_NAME = "gdd-state";
|
|
2642
|
+
var SERVER_VERSION = "1.20.0";
|
|
2643
|
+
function here() {
|
|
2644
|
+
const expectedRel = (0, import_node_path2.join)("sdk", "mcp", "gdd-state");
|
|
2645
|
+
const entry = process.argv[1];
|
|
2646
|
+
if (typeof entry === "string" && entry.length > 0) {
|
|
2647
|
+
const entryDir = (0, import_node_path2.dirname)((0, import_node_path2.resolve)(entry));
|
|
2648
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path2.join)(entryDir, "tools", "index.ts"))) {
|
|
2649
|
+
return entryDir;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
const candidate = (0, import_node_path2.resolve)(process.cwd(), expectedRel);
|
|
2653
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path2.join)(candidate, "tools", "index.ts"))) {
|
|
2654
|
+
return candidate;
|
|
2655
|
+
}
|
|
2656
|
+
return candidate;
|
|
2657
|
+
}
|
|
2658
|
+
function loadTools() {
|
|
2659
|
+
const baseDir = here();
|
|
2660
|
+
return TOOL_MODULES.map((m) => {
|
|
2661
|
+
const absPath = (0, import_node_path2.join)(baseDir, "tools", m.schemaPath);
|
|
2662
|
+
const raw = (0, import_node_fs4.readFileSync)(absPath, "utf8");
|
|
2663
|
+
const parsed = JSON.parse(raw);
|
|
2664
|
+
const rawInput = parsed.properties?.input;
|
|
2665
|
+
const inputSchema = rawInput !== void 0 && typeof rawInput === "object" ? rawInput : { type: "object" };
|
|
2666
|
+
if (!("type" in inputSchema)) inputSchema["type"] = "object";
|
|
2667
|
+
return { ...m, inputSchema };
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
var TOOL_DESCRIPTIONS = {
|
|
2671
|
+
gdd_state__get: "Read current STATE.md (parsed). Read-only; no event emitted. Optionally projects a subset of fields.",
|
|
2672
|
+
gdd_state__update_progress: "Update <position>.task_progress and/or status. Emits state.mutation.",
|
|
2673
|
+
gdd_state__transition_stage: "Run gate and advance <position>.stage on pass. Gate vetoes return {success:false, error:{context:{blockers:[...]}}}; never crashes the server. Emits state.transition.",
|
|
2674
|
+
gdd_state__add_blocker: "Append one entry to <blockers>. Defaults stage to current position.stage and date to today (UTC). Emits state.mutation.",
|
|
2675
|
+
gdd_state__resolve_blocker: "Remove one <blockers> entry by 0-based index or exact text match. Returns operation_failed when no row matches. Emits state.mutation on removal.",
|
|
2676
|
+
gdd_state__add_decision: "Append one entry to <decisions>. Auto-allocates D-N id when not supplied. Emits state.mutation.",
|
|
2677
|
+
gdd_state__add_must_have: "Append one entry to <must_haves>. Auto-allocates M-N id when not supplied. Emits state.mutation.",
|
|
2678
|
+
gdd_state__set_status: "Update <position>.status. Emits state.mutation.",
|
|
2679
|
+
gdd_state__checkpoint: "Update frontmatter.last_checkpoint and append a <timestamps> entry. Emits state.mutation.",
|
|
2680
|
+
gdd_state__probe_connections: "Merge probe results into <connections>. Overwrites keys present in the input; does NOT delete keys not in the input. Emits state.mutation.",
|
|
2681
|
+
gdd_state__frontmatter_update: "Patch one or more frontmatter fields. Rejects pipeline_state_version and stage (use transition_stage). Emits state.mutation."
|
|
2682
|
+
};
|
|
2683
|
+
var TOOL_READONLY = {
|
|
2684
|
+
gdd_state__get: true
|
|
2685
|
+
};
|
|
2686
|
+
function buildServer() {
|
|
2687
|
+
const tools = loadTools();
|
|
2688
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2689
|
+
for (const t of tools) byName.set(t.name, t);
|
|
2690
|
+
const server = new import_server.Server(
|
|
2691
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
2692
|
+
{
|
|
2693
|
+
capabilities: { tools: {} }
|
|
2694
|
+
}
|
|
2695
|
+
);
|
|
2696
|
+
server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => {
|
|
2697
|
+
return {
|
|
2698
|
+
tools: tools.map((t) => {
|
|
2699
|
+
const description = TOOL_DESCRIPTIONS[t.name] ?? t.name;
|
|
2700
|
+
const readOnly = TOOL_READONLY[t.name] ?? false;
|
|
2701
|
+
return {
|
|
2702
|
+
name: t.name,
|
|
2703
|
+
description,
|
|
2704
|
+
inputSchema: t.inputSchema,
|
|
2705
|
+
annotations: {
|
|
2706
|
+
readOnlyHint: readOnly,
|
|
2707
|
+
destructiveHint: !readOnly,
|
|
2708
|
+
idempotentHint: false
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
})
|
|
2712
|
+
};
|
|
2713
|
+
});
|
|
2714
|
+
server.setRequestHandler(import_types8.CallToolRequestSchema, async (req) => {
|
|
2715
|
+
const { name: toolName, arguments: args } = req.params;
|
|
2716
|
+
const tool = byName.get(toolName);
|
|
2717
|
+
if (tool === void 0) {
|
|
2718
|
+
const payload = toToolError(
|
|
2719
|
+
new Error(`unknown tool: ${toolName}`)
|
|
2720
|
+
);
|
|
2721
|
+
return {
|
|
2722
|
+
isError: true,
|
|
2723
|
+
content: [
|
|
2724
|
+
{ type: "text", text: JSON.stringify({ success: false, error: payload.error }) }
|
|
2725
|
+
],
|
|
2726
|
+
structuredContent: { success: false, error: payload.error }
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
let response;
|
|
2730
|
+
try {
|
|
2731
|
+
response = await tool.handle(args ?? {});
|
|
2732
|
+
} catch (err) {
|
|
2733
|
+
const payload = toToolError(err);
|
|
2734
|
+
response = { success: false, error: payload.error };
|
|
2735
|
+
}
|
|
2736
|
+
const text = JSON.stringify(response);
|
|
2737
|
+
if (response.success === true) {
|
|
2738
|
+
return {
|
|
2739
|
+
content: [{ type: "text", text }],
|
|
2740
|
+
structuredContent: response
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
return {
|
|
2744
|
+
isError: true,
|
|
2745
|
+
content: [{ type: "text", text }],
|
|
2746
|
+
structuredContent: response
|
|
2747
|
+
};
|
|
2748
|
+
});
|
|
2749
|
+
return server;
|
|
2750
|
+
}
|
|
2751
|
+
async function runStdio() {
|
|
2752
|
+
const server = buildServer();
|
|
2753
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
2754
|
+
const shutdown = async (signal) => {
|
|
2755
|
+
if (SHUTTING_DOWN) return;
|
|
2756
|
+
SHUTTING_DOWN = true;
|
|
2757
|
+
try {
|
|
2758
|
+
await server.close();
|
|
2759
|
+
} catch {
|
|
2760
|
+
}
|
|
2761
|
+
process.exit(signal === "SIGTERM" ? 0 : 0);
|
|
2762
|
+
};
|
|
2763
|
+
process.on("SIGINT", () => {
|
|
2764
|
+
void shutdown("SIGINT");
|
|
2765
|
+
});
|
|
2766
|
+
process.on("SIGTERM", () => {
|
|
2767
|
+
void shutdown("SIGTERM");
|
|
2768
|
+
});
|
|
2769
|
+
await server.connect(transport);
|
|
2770
|
+
}
|
|
2771
|
+
var SHUTTING_DOWN = false;
|
|
2772
|
+
function isMain() {
|
|
2773
|
+
const entry = process.argv[1];
|
|
2774
|
+
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
2775
|
+
return /sdk\/mcp\/gdd-state\/server\.(ts|js|cjs|mjs)$/.test(
|
|
2776
|
+
entry.replace(/\\/g, "/")
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
if (isMain()) {
|
|
2780
|
+
runStdio().catch((err) => {
|
|
2781
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2782
|
+
console.error(`[gdd-state] fatal: ${msg}`);
|
|
2783
|
+
process.exit(1);
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2787
|
+
0 && (module.exports = {
|
|
2788
|
+
buildServer,
|
|
2789
|
+
runStdio
|
|
2790
|
+
});
|