@namewta/speculo 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -29
- package/dist/src/agent-skills.d.ts +43 -17
- package/dist/src/agent-skills.js +93 -26
- package/dist/src/agent-skills.js.map +1 -1
- package/dist/src/cli.js +43 -27
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +20 -6
- package/dist/src/index.js +36 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/install-ui.d.ts +61 -0
- package/dist/src/install-ui.js +268 -0
- package/dist/src/install-ui.js.map +1 -0
- package/dist/src/interaction.d.ts +5 -0
- package/dist/src/interaction.js +10 -0
- package/dist/src/interaction.js.map +1 -0
- package/dist/src/transaction.d.ts +2 -0
- package/dist/src/transaction.js +16 -0
- package/dist/src/transaction.js.map +1 -1
- package/dist/src/workflows.js +2 -2
- package/dist/src/workflows.js.map +1 -1
- package/package.json +2 -2
- package/template/canonical/canonical-specdev-goal-plan.md +138 -78
- package/template/canonical/canonical-specdev-grill-with-docs.md +818 -184
- package/template/canonical/canonical-specdev-spec.md +819 -185
- package/template/canonical/canonical-specdev-tickets.md +690 -56
- package/template/canonical/canonical-specdev-wayfinder.md +818 -184
- package/template/references/coordination/coordination-contract.md +8 -8
- package/template/references/coordination/modes/cli.md +5 -5
- package/template/references/coordination/modes/herdr.md +16 -1
- package/template/references/coordination/result-and-recovery.md +2 -2
- package/template/references/coordination/task-brief.md +5 -5
- package/template/workflows/specdev/SKILL.md +2 -2
- package/template/workflows/specdev/common/rules/artifact-contract.md +2 -2
- package/template/workflows/specdev/common/rules/change-completion.md +4 -2
- package/template/workflows/specdev/common/rules/domain-runtime.md +2 -2
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -2
- package/template/workflows/specdev/common/rules/execution-policy.md +28 -0
- package/template/workflows/specdev/common/rules/operating-governance.md +2 -2
- package/template/workflows/specdev/common/rules/path-ownership.md +3 -3
- package/template/workflows/specdev/common/rules/workflow-routing.md +3 -3
- package/template/workflows/specdev/common/skills/code-review/SKILL.md +2 -0
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +11 -11
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +20 -12
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +12 -12
- package/template/workflows/specdev/common/tools/README.md +1 -1
- package/template/workflows/specdev/common/tools/change-artifacts.mjs +5 -11
- package/template/workflows/specdev/common/tools/plan-contract.mjs +11 -3
- package/template/workflows/specdev/common/tools/tests/engineering.test.mjs +2 -2
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +131 -77
- package/template/workflows/specdev/manifest.json +4 -6
- package/template/workflows/specdev/specdev-c-code-review/SKILL.md +2 -2
- package/template/workflows/specdev/specdev-i-implement/SKILL.md +5 -5
- package/template/workflows/specdev/specdev-i-implement/assets/evidence-template.md +7 -6
- package/template/workflows/specdev/specdev-i-implement/references/execution-preflight.md +8 -4
- package/template/workflows/specdev/specdev-i-implement/references/implementation-procedure.md +17 -17
- package/template/workflows/specdev/specdev-i-init-setup/SKILL.md +1 -1
- package/template/workflows/specdev/specdev-l-learn/SKILL.md +35 -0
- package/template/workflows/specdev/specdev-l-learn/references/learning-change.md +23 -0
- package/template/workflows/specdev/specdev-l-learn/references/lesson-format.md +61 -0
- package/template/workflows/specdev/specdev-p-goal-plan/SKILL.md +2 -2
- package/template/workflows/specdev/specdev-p-goal-plan/assets/goal-plan-template.md +2 -0
- package/template/workflows/specdev/specdev-p-goal-plan/references/orchestration-protocol.md +1 -1
- package/template/workflows/specdev/specdev-p-goal-plan/references/planning-modes.md +2 -2
- package/template/workflows/specdev/specdev-p-goal-plan/references/single-change-plan.md +2 -2
- package/template/workflows/specdev/specdev-r-retro/SKILL.md +1 -1
- package/template/workflows/specdev/specdev-s-spec/SKILL.md +1 -1
- package/template/workflows/specdev/specdev-t-tickets/SKILL.md +1 -1
- package/template/workflows/specdev/specdev-t-tickets/assets/ticket-template.md +2 -2
- package/template/workflows/specdev/specdev-t-tickets/assets/tickets-map-template.md +1 -1
- package/template/workflows/specdev/specdev-t-tickets/references/planning-procedure.md +1 -1
- package/template/workflows/specdev/specdev-t-tickets/references/ticket-readiness.md +1 -1
- package/template/workflows/specdev/specdev-l-learn-change/SKILL.md +0 -33
- package/template/workflows/specdev/specdev-l-learn-change/references/lesson-format.md +0 -51
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# Candidate Merge And Parent Integration
|
|
2
2
|
|
|
3
|
-
仅由 Lead/integration owner
|
|
3
|
+
仅由 Lead/integration owner 对处于 review 的 Ticket 或 Direct Spec 执行单元调用。Ticket 状态来自原票;Direct Spec 状态来自原 Evidence 的 Workspace Execution Record。unit-id 为真实 Ticket ID 或 direct-spec。
|
|
4
4
|
|
|
5
5
|
## 1. 接收 source checkpoint
|
|
6
6
|
|
|
7
|
-
1.
|
|
7
|
+
1. 核对获批 Spec/Ready Ticket、实际存在的计划、Evidence 目标、owner 与本地 integration 授权;
|
|
8
8
|
2. 验证来源 worktree clean,branch tip 精确等于 `source_checkpoint`,commit 从 `base_sha` 可达;
|
|
9
9
|
3. 审计实际 diff 未越过 writable/shared owner 合同;
|
|
10
10
|
4. 确认 source-worktree 必跑非 E2E 检查已执行,且没有把 E2E 自报为通过;
|
|
11
11
|
5. 重读父分支 checkout clean、HEAD 与 remote/本地约定,记录 `parent_before_sha`。
|
|
12
12
|
|
|
13
|
-
建立新 candidate
|
|
13
|
+
建立新 candidate 前先比较原记录 `attempts` 与有效 Plan 或无 Goal 执行合同的 `integration_attempt_limit`。若前一轮尚未通过且当前 attempts 已达到上限,不创建或重建 candidate、不增加 attempts;保留 source workspace、旧 candidate 与失败记录,将执行记录及已有 Ticket 标为 `blocked`,向有效 Lead 返回 `integration-attempt-limit`。
|
|
14
14
|
|
|
15
15
|
其他预检失败时保持 `review`/`blocked`,不开始候选合并。
|
|
16
16
|
|
|
17
17
|
## 2. 建立 parent-candidate checkout
|
|
18
18
|
|
|
19
|
-
1. 使用 branch `speculo/integration/<change>/<
|
|
19
|
+
1. 使用 branch `speculo/integration/<change>/<unit-id>` 和 locator `specdev-worktree/.integration/<change>/<unit-id>`,从最新 `parent_before_sha` 建立 Lead-owned integration worktree;
|
|
20
20
|
2. 如果父 SHA 是 source checkpoint 的祖先,在 candidate checkout 执行 `git merge --ff-only <source_checkpoint>`,`method=fast-forward`;
|
|
21
21
|
3. 否则执行 `git merge --no-ff --no-commit <source_checkpoint>`;
|
|
22
22
|
4. 冲突按 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/merge-conflict-protocol.md</Path>` 处理。需要新产品决定时执行 `git merge --abort`,记录 blocker 并返回来源 worktree;
|
|
23
23
|
5. 对分叉结果创建一次 Lead-owned candidate merge commit,`method=merge-commit`;
|
|
24
|
-
6. 记录 candidate branch/locator、`candidate_sha`、`source_sha`、冲突路径与 attempts,worktree 状态改为 `integrating`、integration 状态改为 `candidate`。
|
|
24
|
+
6. 记录 candidate branch/locator、`candidate_sha`、实际 Git tree SHA `candidate_tree_sha`、`source_sha`、冲突路径与 attempts,worktree 状态改为 `integrating`、integration 状态改为 `candidate`。
|
|
25
25
|
|
|
26
26
|
重试前从最新父分支重建 candidate branch/worktree;旧 candidate SHA 保存在 Evidence。候选生命周期的重建/回收包含在 local candidate integration 授权中。
|
|
27
27
|
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
|
|
30
30
|
在 candidate checkout 运行:
|
|
31
31
|
|
|
32
|
-
-
|
|
32
|
+
- 当前实现单元受影响集成与回归;
|
|
33
33
|
- 项目要求的 typecheck/lint/build 或其他父状态检查;
|
|
34
|
-
- 仅当 Ticket
|
|
34
|
+
- 仅当 Spec/Ticket/有效执行合同 `e2e.required=true` 时运行对应 E2E。
|
|
35
35
|
|
|
36
|
-
每条命令记录运行环境 `parent-candidate`、退出码与摘要。E2E required 未运行或失败时 integration `verification=failed`、`status=failed`;父分支保持 `parent_before_sha`。当本轮失败使 attempts
|
|
36
|
+
每条命令记录运行环境 `parent-candidate`、退出码与摘要。E2E required 未运行或失败时 integration `verification=failed`、`status=failed`;父分支保持 `parent_before_sha`。当本轮失败使 attempts 达到有效执行合同的 `integration_attempt_limit` 时,保存本轮失败并返回 Lead 复盘;不得继续机械修正、放宽断言、删除检查或发明行为。上限是 Lead 复盘触发点,不是永久禁止恢复。
|
|
37
37
|
|
|
38
38
|
## 4. 推进父分支
|
|
39
39
|
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
1. 重读父分支 HEAD;不等于 `parent_before_sha` 时将 candidate 标记 `stale`,不推进父分支并从步骤 2 重建;
|
|
43
43
|
2. 在父分支 checkout 执行 `git merge --ff-only <candidate_sha>`;候选 merge commit 本身已以父 SHA 为第一祖先,因此不再创建第二个 merge commit;
|
|
44
44
|
3. 重读父 HEAD、tree 与 ancestor 关系,确认 HEAD 精确等于 candidate SHA 且包含 source checkpoint;
|
|
45
|
-
4. 写入 `result_sha=candidate_sha`、`verification=passed`、E2E
|
|
46
|
-
5. integration/status 改为 `passed`/`integrated`,再由 Lead 标记 Ticket Done。
|
|
45
|
+
4. 写入 `result_sha=candidate_sha`、`verification=passed`、full_suite/E2E 最终状态、`promotion_status=applied` 和 Evidence;
|
|
46
|
+
5. integration/status 改为 `passed`/`integrated`,再由 Lead 标记 Ticket Done 或在 Direct Spec 全部 AC 通过后完成 change。
|
|
47
47
|
|
|
48
48
|
## 5. 失败、清理与恢复
|
|
49
49
|
|
|
50
|
-
- candidate
|
|
51
|
-
- 达到 integration attempt 上限:保留全部 source/candidate checkpoint 与失败记录,等待 Lead
|
|
50
|
+
- candidate 检查失败:父分支不动,已有 Ticket 回 `in_progress` 或 `blocked`,来源 worktree 保留;
|
|
51
|
+
- 达到 integration attempt 上限:保留全部 source/candidate checkpoint 与失败记录,等待 Lead 在原 Evidence 写明共同失败模式、最可能原因、下一轮改变和下一 owner/路由;只有形成有实质变化的新 Dispatch Packet 后,Lead 才可将当前执行记录 `attempts` 重置为 `0` 并重新进入 finalize;
|
|
52
52
|
- 父 HEAD 漂移:旧 candidate 记 `stale`,完整重建并重跑;
|
|
53
53
|
- 成功后可按 candidate integration 授权回收 transient integration worktree/branch;来源 branch/worktree 不自动清理。获得独立 cleanup 授权并清理后,只将生命周期状态改为 `removed`,完整保留已经通过的集成与 E2E 证据;
|
|
54
54
|
- push、PR、remote merge、deploy、migration 和生产动作仍需各自授权。
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
node <Path>{roots.workflows}/specdev/common/tools/validate-specdev.mjs</Path> \
|
|
7
|
-
--stage <triage|diagnosis|grill|spec|tickets|goal-plan|implement|learn
|
|
7
|
+
--stage <triage|diagnosis|grill|spec|tickets|goal-plan|implement|learn|retro|pr-delivery|review|prototype|wayfinder|complete|archive-ready|archived> \
|
|
8
8
|
--repo <project-root> \
|
|
9
9
|
<Path>{roots.state}/specdev/changes/{change}</Path>
|
|
10
10
|
```
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
|
+
import { readChangeExecution } from './plan-contract.mjs';
|
|
5
6
|
import {
|
|
6
7
|
readChangeFactsSync, readArtifactSectionSync, readUtf8ArtifactSync,
|
|
7
8
|
safeWorkPathSync, markdownBodyLines, parseFrontmatter,
|
|
@@ -105,16 +106,9 @@ function validateCancellation(facts, owner, errors) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
for (const key of ['Dependencies','Checkpoints','External']) if (!/^(?:none|resolved|transferred|waived): \S/.test(data[key] ?? '')) errors.push(`closure-evidence-missing: ${facts.readmePath}: unresolved ${key}`);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const blocks = [...text.matchAll(/^## Workspace Execution Record\r?\n([\s\S]*?)(?=^## |$(?![\s\S]))/gm)];
|
|
112
|
-
for (const block of blocks) {
|
|
113
|
-
const json = /```json\s*\n([\s\S]*?)\n```/.exec(block[1]);
|
|
114
|
-
if (!json) error('closure-evidence-missing', path, 'invalid workspace execution record');
|
|
115
|
-
const record = JSON.parse(json[1]);
|
|
116
|
-
if (['active','review','integrating','blocked'].includes(record.status)) errors.push(`writer-active: ${path}: cancellation does not end an execution owner or candidate`);
|
|
117
|
-
}
|
|
109
|
+
for (const record of readChangeExecution(root)) {
|
|
110
|
+
if (record.purpose === "direct-spec" && record.status === "cancelled" && !/^stopped: \S/.test(data.Writers ?? "")) errors.push(`writer-active: ${root}: cancelled Direct Spec execution requires observed stopped writer evidence`);
|
|
111
|
+
if (['active','review','integrating','blocked'].includes(record.status)) errors.push(`writer-active: ${root}: ${record.ticket_id ?? 'direct-spec'} cancellation does not end an execution owner or candidate`);
|
|
118
112
|
}
|
|
119
113
|
}
|
|
120
114
|
export function validateArchiveDelivery(changeRoot) {
|
|
@@ -152,7 +146,7 @@ export function validateChangeClosure({context, change, forArchive = false}) {
|
|
|
152
146
|
const goalPath = safeWorkPathSync(root, 'goal-plan.md', {type:'file'});
|
|
153
147
|
const lightweightGoal = existsSync(goalPath) && parseFrontmatter(readUtf8ArtifactSync(goalPath)).fields.artifact === 'lightweight-goal-plan';
|
|
154
148
|
const independent = ['spec.md','ticket','tickets-map.md','implementation-plan.md','implementation-map.md','retro',...(!lightweightGoal ? ['goal-plan.md'] : [])].some(name => existsSync(join(root, name)));
|
|
155
|
-
const complex = independent || ['DESIGN.md','prototypes'].some(name => existsSync(join(root,name)));
|
|
149
|
+
const complex = independent || readChangeExecution(root).length > 0 || ['DESIGN.md','prototypes'].some(name => existsSync(join(root,name)));
|
|
156
150
|
scope = complex ? 'complex-preflight' : 'lightweight';
|
|
157
151
|
if (!independent && facts.outcome === 'completed') {
|
|
158
152
|
validateLightweightCompletion(root, errors);
|
|
@@ -262,13 +262,13 @@ export function validateInitiative(directory) {
|
|
|
262
262
|
return errors;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
/** Read actual
|
|
265
|
+
/** Read actual Ticket or Direct Spec records; never creates empty execution slots. */
|
|
266
266
|
export function readChangeExecution(changeRoot) {
|
|
267
267
|
const root = join(changeRoot, 'evidence');
|
|
268
268
|
if (!existsSync(root)) return [];
|
|
269
269
|
if (lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) throw new Error('Evidence must be a real directory');
|
|
270
270
|
const records = [];
|
|
271
|
-
for (const file of readdirSync(root).filter(name => /^T-\d{2,}\.md$/.test(name)).sort()) {
|
|
271
|
+
for (const file of readdirSync(root).filter(name => /^T-\d{2,}\.md$/.test(name) || name === 'direct-spec.md').sort()) {
|
|
272
272
|
const path = join(root, file);
|
|
273
273
|
if (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) throw new Error(`${path}: Evidence must be a regular file`);
|
|
274
274
|
const text = readFileSync(path, 'utf8').replaceAll('\r\n', '\n');
|
|
@@ -276,9 +276,17 @@ export function readChangeExecution(changeRoot) {
|
|
|
276
276
|
if (!sections.length) continue;
|
|
277
277
|
if (sections.length !== 1) throw new Error(`${path}: duplicate Workspace Execution Record`);
|
|
278
278
|
const record = readEvidenceValue(text,'## Workspace Execution Record');
|
|
279
|
-
|
|
279
|
+
const direct = file === 'direct-spec.md';
|
|
280
|
+
if (!plainObject(record) || (direct
|
|
281
|
+
? record.purpose !== 'direct-spec' || record.change !== basename(changeRoot) || 'ticket_id' in record
|
|
282
|
+
: record.ticket_id !== file.slice(0, -3) || 'purpose' in record || 'change' in record)) {
|
|
283
|
+
throw new Error(`${path}: execution record identity differs from Evidence ${direct ? 'Direct Spec/change' : 'Ticket'}`);
|
|
284
|
+
}
|
|
280
285
|
records.push(record);
|
|
281
286
|
}
|
|
287
|
+
if (records.some(record => record.purpose === 'direct-spec') && records.some(record => record.ticket_id)) {
|
|
288
|
+
throw new Error('Direct Spec and Ticket execution records cannot coexist; reconcile the original execution before changing modes');
|
|
289
|
+
}
|
|
282
290
|
return records;
|
|
283
291
|
}
|
|
284
292
|
|
|
@@ -223,10 +223,10 @@ test('range comparison and merge-base comparison have distinct semantics', t =>
|
|
|
223
223
|
const range = resolveReviewScope({ repo: r.root, mode: 'range', base: r.c, head: side });
|
|
224
224
|
assert.deepEqual(baseline.changed_paths, ['side.txt']); assert.deepEqual(range.changed_paths, ['b.txt', 'c.txt', 'side.txt']);
|
|
225
225
|
});
|
|
226
|
-
test('fifteen Work IDs and persisted schema versions remain unchanged', () => {
|
|
226
|
+
test('fifteen Work IDs match the current registry and persisted schema versions remain unchanged', () => {
|
|
227
227
|
const entries = readdirSync(workflow).filter(name => /^specdev-[a-z]-/.test(name)); assert.equal(entries.length, 15);
|
|
228
228
|
const ids = entries.map(name => /speculo-id: (.+)$/m.exec(readFileSync(join(workflow, name, 'SKILL.md'), 'utf8'))?.[1]);
|
|
229
|
-
assert.deepEqual(ids.sort(), ['archive-and-consolidate','code-review','diagnose-bugs','goal-plan','grill-with-docs','implement','init-setup','learn
|
|
229
|
+
assert.deepEqual(ids.sort(), ['archive-and-consolidate','code-review','diagnose-bugs','goal-plan','grill-with-docs','implement','init-setup','learn','prototype','retro','review-architecture','spec','tickets','triage','wayfinder'].map(id => 'specdev/' + id).sort());
|
|
230
230
|
for (const [file, version] of [['config', 7], ['status', 5], ['change-status', 6], ['goal-plan', 7]]) {
|
|
231
231
|
const schema = JSON.parse(readFileSync(join(workflow, 'common/schemas', `${file}.schema.json`), 'utf8'));
|
|
232
232
|
assert.equal(schema.properties.schema_version.const, version);
|