@birdie_moblie/open_spec 2.1.7

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.
Files changed (186) hide show
  1. package/.agents/plugins/marketplace.json +13 -0
  2. package/.claude-plugin/marketplace.json +12 -0
  3. package/.claude-plugin/plugin.json +9 -0
  4. package/.codex-plugin/plugin.json +9 -0
  5. package/LICENSE +21 -0
  6. package/README.md +111 -0
  7. package/bin/openspec.js +3 -0
  8. package/dist/cli/commands/change.d.ts +3 -0
  9. package/dist/cli/commands/change.js +37 -0
  10. package/dist/cli/commands/ext.d.ts +3 -0
  11. package/dist/cli/commands/ext.js +196 -0
  12. package/dist/cli/commands/figma.d.ts +3 -0
  13. package/dist/cli/commands/figma.js +73 -0
  14. package/dist/cli/commands/init.d.ts +3 -0
  15. package/dist/cli/commands/init.js +71 -0
  16. package/dist/cli/commands/opsx.d.ts +3 -0
  17. package/dist/cli/commands/opsx.js +324 -0
  18. package/dist/cli/commands/usage.d.ts +3 -0
  19. package/dist/cli/commands/usage.js +21 -0
  20. package/dist/cli/output.d.ts +8 -0
  21. package/dist/cli/output.js +17 -0
  22. package/dist/cli/program.d.ts +4 -0
  23. package/dist/cli/program.js +42 -0
  24. package/dist/cli/register.d.ts +3 -0
  25. package/dist/cli/register.js +13 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.js +3 -0
  28. package/dist/config/load.d.ts +7 -0
  29. package/dist/config/load.js +55 -0
  30. package/dist/config/presets.d.ts +129 -0
  31. package/dist/config/presets.js +125 -0
  32. package/dist/config/schema.d.ts +281 -0
  33. package/dist/config/schema.js +178 -0
  34. package/dist/core/archive.d.ts +5 -0
  35. package/dist/core/archive.js +33 -0
  36. package/dist/core/change.d.ts +12 -0
  37. package/dist/core/change.js +69 -0
  38. package/dist/core/code-changes.d.ts +9 -0
  39. package/dist/core/code-changes.js +31 -0
  40. package/dist/core/flow.d.ts +6 -0
  41. package/dist/core/flow.js +99 -0
  42. package/dist/core/gates.d.ts +5 -0
  43. package/dist/core/gates.js +71 -0
  44. package/dist/core/instructions.d.ts +21 -0
  45. package/dist/core/instructions.js +102 -0
  46. package/dist/core/source-operations.d.ts +25 -0
  47. package/dist/core/source-operations.js +94 -0
  48. package/dist/core/sources.d.ts +33 -0
  49. package/dist/core/sources.js +234 -0
  50. package/dist/core/state.d.ts +15 -0
  51. package/dist/core/state.js +75 -0
  52. package/dist/core/status.d.ts +31 -0
  53. package/dist/core/status.js +22 -0
  54. package/dist/core/tasks.d.ts +15 -0
  55. package/dist/core/tasks.js +143 -0
  56. package/dist/core/todo.d.ts +31 -0
  57. package/dist/core/todo.js +114 -0
  58. package/dist/core/types.d.ts +297 -0
  59. package/dist/core/types.js +227 -0
  60. package/dist/core/verify.d.ts +229 -0
  61. package/dist/core/verify.js +391 -0
  62. package/dist/core/worker-evidence.d.ts +71 -0
  63. package/dist/core/worker-evidence.js +253 -0
  64. package/dist/core/worker.d.ts +22 -0
  65. package/dist/core/worker.js +478 -0
  66. package/dist/delivery/claude-install.d.ts +4 -0
  67. package/dist/delivery/claude-install.js +44 -0
  68. package/dist/delivery/distribution.d.ts +3 -0
  69. package/dist/delivery/distribution.js +53 -0
  70. package/dist/delivery/skills.d.ts +36 -0
  71. package/dist/delivery/skills.js +178 -0
  72. package/dist/figma/cache.d.ts +19 -0
  73. package/dist/figma/cache.js +115 -0
  74. package/dist/figma/claude.d.ts +14 -0
  75. package/dist/figma/claude.js +121 -0
  76. package/dist/figma/collect.d.ts +3 -0
  77. package/dist/figma/collect.js +57 -0
  78. package/dist/figma/index.d.ts +35 -0
  79. package/dist/figma/index.js +178 -0
  80. package/dist/figma/media.d.ts +5 -0
  81. package/dist/figma/media.js +77 -0
  82. package/dist/figma/response.d.ts +9 -0
  83. package/dist/figma/response.js +27 -0
  84. package/dist/hooks/command-context.d.ts +19 -0
  85. package/dist/hooks/command-context.js +33 -0
  86. package/dist/hooks/engine.d.ts +61 -0
  87. package/dist/hooks/engine.js +314 -0
  88. package/dist/hooks/plugin-runtime.d.ts +188 -0
  89. package/dist/hooks/plugin-runtime.js +198 -0
  90. package/dist/index.d.ts +99 -0
  91. package/dist/index.js +185 -0
  92. package/dist/marketplace/adapters.d.ts +202 -0
  93. package/dist/marketplace/adapters.js +203 -0
  94. package/dist/marketplace/builtin.d.ts +9 -0
  95. package/dist/marketplace/builtin.js +9 -0
  96. package/dist/marketplace/install.d.ts +38 -0
  97. package/dist/marketplace/install.js +108 -0
  98. package/dist/marketplace/registry.d.ts +71 -0
  99. package/dist/marketplace/registry.js +342 -0
  100. package/dist/marketplace/upgrade.d.ts +39 -0
  101. package/dist/marketplace/upgrade.js +104 -0
  102. package/dist/shared/command-log.d.ts +9 -0
  103. package/dist/shared/command-log.js +55 -0
  104. package/dist/shared/errors.d.ts +7 -0
  105. package/dist/shared/errors.js +14 -0
  106. package/dist/shared/exec.d.ts +16 -0
  107. package/dist/shared/exec.js +86 -0
  108. package/dist/shared/fs.d.ts +18 -0
  109. package/dist/shared/fs.js +93 -0
  110. package/dist/shared/git.d.ts +22 -0
  111. package/dist/shared/git.js +132 -0
  112. package/dist/shared/hash.d.ts +5 -0
  113. package/dist/shared/hash.js +29 -0
  114. package/dist/shared/paths.d.ts +16 -0
  115. package/dist/shared/paths.js +97 -0
  116. package/dist/shared/pkg.d.ts +3 -0
  117. package/dist/shared/pkg.js +23 -0
  118. package/dist/shared/version.d.ts +9 -0
  119. package/dist/shared/version.js +34 -0
  120. package/dist/usage/analyze.d.ts +5 -0
  121. package/dist/usage/analyze.js +212 -0
  122. package/dist/usage/claude.d.ts +9 -0
  123. package/dist/usage/claude.js +59 -0
  124. package/dist/usage/collect.d.ts +11 -0
  125. package/dist/usage/collect.js +71 -0
  126. package/dist/usage/doctor.d.ts +72 -0
  127. package/dist/usage/doctor.js +121 -0
  128. package/dist/usage/parse.d.ts +28 -0
  129. package/dist/usage/parse.js +345 -0
  130. package/dist/usage/types.d.ts +103 -0
  131. package/dist/usage/types.js +2 -0
  132. package/docs/getting-started.md +20 -0
  133. package/docs/guides/configuration.md +33 -0
  134. package/docs/guides/cost-benchmark.md +88 -0
  135. package/docs/guides/plugin-migration.md +65 -0
  136. package/docs/guides/smart-figma.md +51 -0
  137. package/docs/guides/source-reading.md +35 -0
  138. package/docs/guides/ui-conventions-template.md +39 -0
  139. package/docs/guides/worker-evidence.md +73 -0
  140. package/docs/guides/workflow.md +74 -0
  141. package/docs/maintainers/architecture.md +27 -0
  142. package/docs/maintainers/plugin-runtime-contract.md +55 -0
  143. package/package.json +78 -0
  144. package/plugin.json +9 -0
  145. package/presets/backend-service.yaml +19 -0
  146. package/presets/flutter-mobile/plugin-hooks.json +68 -0
  147. package/presets/flutter-mobile/ui-conventions.md +122 -0
  148. package/presets/flutter-mobile.yaml +205 -0
  149. package/presets/web-product.yaml +23 -0
  150. package/skills/opsx-apply/SKILL.md +35 -0
  151. package/skills/opsx-apply/nodes/blocked.md +7 -0
  152. package/skills/opsx-apply/nodes/code-task.md +29 -0
  153. package/skills/opsx-apply/nodes/gate-a.md +9 -0
  154. package/skills/opsx-apply/nodes/stop.md +5 -0
  155. package/skills/opsx-apply/nodes/task-build.md +50 -0
  156. package/skills/opsx-apply/nodes/task-repair.md +20 -0
  157. package/skills/opsx-apply/nodes/task-scout.md +28 -0
  158. package/skills/opsx-apply/nodes/task-verify.md +38 -0
  159. package/skills/opsx-archive/SKILL.md +24 -0
  160. package/skills/opsx-archive/nodes/archive.md +15 -0
  161. package/skills/opsx-explore/SKILL.md +20 -0
  162. package/skills/opsx-explore/nodes/explore.md +3 -0
  163. package/skills/opsx-fix-verify/SKILL.md +30 -0
  164. package/skills/opsx-fix-verify/nodes/repair.md +11 -0
  165. package/skills/opsx-fix-verify/nodes/reverify.md +3 -0
  166. package/skills/opsx-fix-verify/nodes/stop.md +3 -0
  167. package/skills/opsx-gate-decision/SKILL.md +20 -0
  168. package/skills/opsx-gate-decision/nodes/decide.md +15 -0
  169. package/skills/opsx-propose/SKILL.md +36 -0
  170. package/skills/opsx-propose/nodes/blocked.md +13 -0
  171. package/skills/opsx-propose/nodes/collect.md +35 -0
  172. package/skills/opsx-propose/nodes/parse.md +72 -0
  173. package/skills/opsx-propose/nodes/plan.md +65 -0
  174. package/skills/opsx-propose/nodes/stop.md +17 -0
  175. package/skills/opsx-propose/nodes/tech.md +77 -0
  176. package/skills/opsx-propose/scripts/read-json-source.mjs +22 -0
  177. package/skills/opsx-resume/SKILL.md +21 -0
  178. package/skills/opsx-resume/nodes/route.md +12 -0
  179. package/skills/opsx-source-update/SKILL.md +23 -0
  180. package/skills/opsx-source-update/nodes/update.md +14 -0
  181. package/skills/opsx-verify/SKILL.md +32 -0
  182. package/skills/opsx-verify/nodes/blocked.md +3 -0
  183. package/skills/opsx-verify/nodes/failed.md +3 -0
  184. package/skills/opsx-verify/nodes/gate-b.md +7 -0
  185. package/skills/opsx-verify/nodes/stop.md +5 -0
  186. package/skills/opsx-verify/nodes/verify.md +10 -0
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { type WorkerPacket } from './types.js';
3
+ /** 本 task 已做过几次独立验收(每次 `worker review` 一条记录);控制会话据此判断返修上限,不凭记忆。 */
4
+ export declare function reviewRounds(root: string, change: string, packet: WorkerPacket): number;
5
+ export declare function repairRounds(root: string, change: string, packet: WorkerPacket): number;
6
+ export declare function prepareWorkerEvidence(root: string, change: string, packet: WorkerPacket): void;
7
+ /** Runs once, stores full output on disk, and reuses only successful unchanged receipts. */
8
+ export declare function runWorkerCheck(root: string, change: string, id: string, command?: string, force?: boolean, inputFile?: string): {
9
+ cached: boolean;
10
+ id: string;
11
+ runId: string;
12
+ command: string;
13
+ code: number;
14
+ ok: boolean;
15
+ fingerprint: string;
16
+ log: string;
17
+ logDigest: string;
18
+ at: string;
19
+ outputSummary?: string | undefined;
20
+ workerId?: string | undefined;
21
+ plugin?: {
22
+ path: string;
23
+ sha256: string;
24
+ binding: {
25
+ use: string;
26
+ kind: "hook" | "adapter";
27
+ operation: "preview" | "hook" | "read";
28
+ identity: {
29
+ name: string;
30
+ marketplace: string;
31
+ resolvedSha: string;
32
+ digest: string;
33
+ };
34
+ input?: Record<string, unknown> | undefined;
35
+ inputFiles?: string[] | undefined;
36
+ inputDescription?: string | undefined;
37
+ };
38
+ workerId?: string | undefined;
39
+ stepId?: string | undefined;
40
+ } | undefined;
41
+ context?: {
42
+ path: string;
43
+ digest: string;
44
+ } | undefined;
45
+ };
46
+ /** Reviewer updates are merged by id: omitting an old issue cannot resolve it. An empty array records a clean pass. */
47
+ export declare function recordWorkerReview(root: string, change: string, updates: unknown, repaired?: boolean): {
48
+ path: string;
49
+ open: string[];
50
+ blocking: string[];
51
+ advisory: string[];
52
+ rounds: number;
53
+ repairRounds: number;
54
+ };
55
+ export declare function validateWorkerEvidence(root: string, change: string, packet: WorkerPacket): void;
56
+ /** Final disclosure comes from review records, not optional prose in a worker result. */
57
+ export declare const VisualAdvisorySchema: z.ZodObject<{
58
+ id: z.ZodString;
59
+ status: z.ZodEnum<{
60
+ open: "open";
61
+ resolved: "resolved";
62
+ }>;
63
+ description: z.ZodString;
64
+ advisory: z.ZodOptional<z.ZodLiteral<true>>;
65
+ evidence: z.ZodOptional<z.ZodString>;
66
+ evidenceDigest: z.ZodOptional<z.ZodString>;
67
+ fingerprint: z.ZodOptional<z.ZodString>;
68
+ stepId: z.ZodString;
69
+ }, z.core.$strict>;
70
+ export declare function visualAdvisories(root: string, change: string): Array<z.infer<typeof VisualAdvisorySchema>>;
71
+ //# sourceMappingURL=worker-evidence.d.ts.map
@@ -0,0 +1,253 @@
1
+ import path from 'node:path';
2
+ import { savePluginEvidence, PluginEvidenceSchema, runPluginCommand, pluginEvidenceCurrent } from '../hooks/plugin-runtime.js';
3
+ import { operationContext } from './source-operations.js';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import { z } from 'zod';
7
+ import { exists, readJson, writeJson } from '../shared/fs.js';
8
+ import { sha256File } from '../shared/hash.js';
9
+ import { checkoutFingerprint as fingerprint } from '../shared/git.js';
10
+ import { resolveInside } from '../shared/paths.js';
11
+ import { splitCommand } from '../shared/exec.js';
12
+ import { runLoggedCommand } from '../shared/command-log.js';
13
+ import { AppError } from '../shared/errors.js';
14
+ import { artifactRel, requireChange } from './change.js';
15
+ import { appendEvent } from './state.js';
16
+ import { WorkerPacketSchema } from './types.js';
17
+ import { CommandContextEvidenceSchema, commandContextCurrent, prepareCommandContext } from '../hooks/command-context.js';
18
+ const Id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
19
+ const Run = z.object({
20
+ id: Id, runId: z.string(), command: z.string(), code: z.number().int(), ok: z.boolean(),
21
+ fingerprint: z.string(), log: z.string(), logDigest: z.string(), at: z.string(),
22
+ outputSummary: z.string().optional(),
23
+ workerId: z.string().optional(),
24
+ plugin: PluginEvidenceSchema.optional(),
25
+ context: CommandContextEvidenceSchema.optional(),
26
+ }).strict();
27
+ const Issue = z.object({
28
+ id: Id, status: z.enum(['open', 'resolved']), description: z.string().min(1),
29
+ advisory: z.literal(true).optional(),
30
+ evidence: z.string().optional(), evidenceDigest: z.string().optional(), fingerprint: z.string().optional(),
31
+ }).strict();
32
+ const Ledger = z.object({
33
+ runs: z.record(z.string(), Run), issues: z.record(z.string(), Issue),
34
+ }).strict();
35
+ // updates 允许为空:一次独立验收没有发现问题也要留下记录,轮次统计以记录数为准。
36
+ const ReviewRecord = z.object({ at: z.string(), updates: z.array(Issue), codeFingerprint: z.string().optional(), repair: z.boolean().optional() }).strict();
37
+ const REVIEW_FILE = /^review-[0-9a-f-]{36}\.json$/;
38
+ function location(root, change, packet) {
39
+ const state = requireChange(root, change);
40
+ const directory = artifactRel(change, `.opsx/execution/r${state.sourceRevision}/${packet.stepId}`);
41
+ const relative = `${directory}/index.json`;
42
+ return { directory, relative, absolute: resolveInside(root, relative) };
43
+ }
44
+ function load(root, change, packet) {
45
+ const paths = location(root, change, packet);
46
+ const ledger = exists(paths.absolute) ? Ledger.parse(readJson(paths.absolute)) : { runs: {}, issues: {} };
47
+ // Each command/review writes its own atomic record. The shared index is only a view:
48
+ // concurrent commands must not erase a failure or a review update by replacing it.
49
+ const directory = resolveInside(root, paths.directory);
50
+ const names = exists(directory) ? readdirSync(directory).sort() : [];
51
+ const runs = names.filter((name) => /^[0-9a-f-]{36}\.json$/.test(name))
52
+ .map((name) => Run.parse(readJson(resolveInside(root, `${paths.directory}/${name}`))))
53
+ .sort((a, b) => a.at.localeCompare(b.at) || a.runId.localeCompare(b.runId));
54
+ for (const run of runs)
55
+ ledger.runs[run.id] = run;
56
+ const reviews = names.filter((name) => REVIEW_FILE.test(name))
57
+ .map((name) => ({ name, record: ReviewRecord.parse(readJson(resolveInside(root, `${paths.directory}/${name}`))) }))
58
+ .sort((a, b) => a.record.at.localeCompare(b.record.at) || a.name.localeCompare(b.name));
59
+ for (const { record } of reviews)
60
+ for (const issue of record.updates)
61
+ ledger.issues[issue.id] = issue;
62
+ return { ...paths, ledger, rounds: reviews.length, repairRounds: repairRoundsAcrossRevisions(root, change, packet.stepId), lastReview: reviews.at(-1)?.record };
63
+ }
64
+ /**
65
+ * The two-repair cost guard belongs to the task, not to one source revision: a cancel → source update →
66
+ * re-plan keeps the same task id and its retained files, so earlier actual repairs still count.
67
+ * Runs and issues stay per revision because the sources they were checked against changed.
68
+ */
69
+ function repairRoundsAcrossRevisions(root, change, stepId) {
70
+ const executionDir = resolveInside(root, artifactRel(change, '.opsx/execution'));
71
+ if (!exists(executionDir))
72
+ return 0;
73
+ let count = 0;
74
+ for (const revision of readdirSync(executionDir).filter((name) => /^r\d+$/.test(name))) {
75
+ const directory = path.join(executionDir, revision, stepId);
76
+ if (!exists(directory))
77
+ continue;
78
+ for (const name of readdirSync(directory).filter((file) => REVIEW_FILE.test(file))) {
79
+ if (ReviewRecord.parse(readJson(path.join(directory, name))).repair)
80
+ count += 1;
81
+ }
82
+ }
83
+ return count;
84
+ }
85
+ /** 本 task 已做过几次独立验收(每次 `worker review` 一条记录);控制会话据此判断返修上限,不凭记忆。 */
86
+ export function reviewRounds(root, change, packet) {
87
+ if (!packet.executionEvidence)
88
+ return 0;
89
+ return load(root, change, packet).rounds;
90
+ }
91
+ export function repairRounds(root, change, packet) {
92
+ return packet.executionEvidence ? load(root, change, packet).repairRounds : 0;
93
+ }
94
+ export function prepareWorkerEvidence(root, change, packet) {
95
+ if (!packet.executionEvidence)
96
+ return;
97
+ const store = load(root, change, packet);
98
+ // Redispatch must expose the latest records before the next worker reads the index.
99
+ writeJson(store.absolute, store.ledger);
100
+ packet.refs.executionEvidence = store.relative;
101
+ }
102
+ function activePacket(root, change) {
103
+ const state = requireChange(root, change);
104
+ if (!state.activeWorker)
105
+ throw new AppError('no_active_worker', '当前没有待验收的 worker');
106
+ const packet = WorkerPacketSchema.parse(readJson(resolveInside(root, state.activeWorker.packet)));
107
+ if (packet.workerId !== state.activeWorker.workerId || packet.stepId !== state.activeWorker.stepId || packet.changeName !== change
108
+ || !packet.executionEvidence)
109
+ throw new AppError('worker_evidence_unavailable', '检查回执仅用于启用 executionEvidence 的 Worker packet');
110
+ return packet;
111
+ }
112
+ function validArtifact(root, relative, digest) {
113
+ const absolute = resolveInside(root, relative, 'worker_evidence_outside_project');
114
+ return exists(absolute) && statSync(absolute).isFile() && sha256File(absolute) === digest;
115
+ }
116
+ /** Runs once, stores full output on disk, and reuses only successful unchanged receipts. */
117
+ export function runWorkerCheck(root, change, id, command, force = false, inputFile) {
118
+ Id.parse(id);
119
+ const packet = activePacket(root, change);
120
+ const hook = packet.initContext?.hooks.find((item) => item.id === id);
121
+ if (hook?.type === 'command') {
122
+ if (command && command !== hook.command)
123
+ throw new AppError('worker_check_command_mismatch', '不能覆盖 packet 中的 command Hook');
124
+ command = hook.command;
125
+ }
126
+ if (!command?.trim())
127
+ throw new AppError('worker_check_command_missing', '提供 --command 或使用 command Hook id');
128
+ if (inputFile && !hook?.plugin)
129
+ throw new AppError('worker_check_input_unsupported', '--input 仅用于插件 command Hook');
130
+ const executable = hook?.plugin ? undefined : splitCommand(command);
131
+ const store = load(root, change, packet);
132
+ const before = fingerprint(root);
133
+ const previous = store.ledger.runs[id];
134
+ if (!hook?.plugin && !force && previous?.ok && (previous.workerId === packet.workerId || (!packet.executionEvidenceVersion && !previous.workerId)) && previous.command === command && previous.fingerprint === before
135
+ && validArtifact(root, previous.log, previous.logDigest) && (!previous.context || commandContextCurrent(root, previous.context)))
136
+ return { ...previous, cached: true };
137
+ const runId = randomUUID();
138
+ const log = `${store.directory}/${runId}.log`;
139
+ const prepared = hook?.plugin ? operationContext(root, change, hook.plugin, resolveInside(root, `${store.directory}/${runId}`), inputFile, { workerId: packet.workerId, stepId: packet.stepId }) : undefined;
140
+ const pluginResult = hook?.plugin && prepared ? runPluginCommand(root, hook.plugin, prepared.context, prepared.files) : undefined;
141
+ const context = hook?.plugin ? undefined : prepareCommandContext(root, change, `${store.directory}/${runId}.context.json`, runId, id, packet);
142
+ const result = pluginResult ? { ...pluginResult, stdout: `完整结果: ${path.relative(root, pluginResult.stdoutPath)}`, stderr: `诊断: ${path.relative(root, pluginResult.stderrPath)}` }
143
+ : runLoggedCommand(executable.bin, executable.args, root, resolveInside(root, log), 0, context?.env);
144
+ const actualLog = pluginResult ? path.relative(root, pluginResult.stdoutPath) : log;
145
+ const run = Run.parse({ id, runId, workerId: packet.workerId, command, code: result.code, ok: result.ok,
146
+ ...(pluginResult ? { plugin: savePluginEvidence(root, prepared.context.outputDir, pluginResult.receipt) } : {}),
147
+ ...(context ? { context: context.evidence } : {}),
148
+ outputSummary: [result.stdout, result.stderr].filter(Boolean).join('\n'),
149
+ fingerprint: fingerprint(root), log: actualLog, logDigest: sha256File(resolveInside(root, actualLog)), at: new Date().toISOString() });
150
+ // Keep every attempt; rebuild the view from independent records after publishing.
151
+ writeJson(resolveInside(root, `${store.directory}/${runId}.json`), run);
152
+ const latest = load(root, change, packet);
153
+ writeJson(latest.absolute, latest.ledger);
154
+ appendEvent(root, change, 'worker.check', { workerId: packet.workerId, id, runId, ok: run.ok, log: actualLog });
155
+ return { ...run, cached: false };
156
+ }
157
+ /** Reviewer updates are merged by id: omitting an old issue cannot resolve it. An empty array records a clean pass. */
158
+ export function recordWorkerReview(root, change, updates, repaired = false) {
159
+ const parsed = z.array(Issue.omit({ evidenceDigest: true, fingerprint: true })).parse(updates);
160
+ if (new Set(parsed.map((item) => item.id)).size !== parsed.length)
161
+ throw new AppError('duplicate_review_issue', '同次更新不能重复问题 id');
162
+ const packet = activePacket(root, change);
163
+ const store = load(root, change, packet);
164
+ const current = fingerprint(root);
165
+ if (repaired && !store.lastReview)
166
+ throw new AppError('initial_review_required', '首次独立验收不能计为返修');
167
+ const actualRepair = repaired && store.lastReview?.codeFingerprint !== current;
168
+ if (actualRepair && packet.task?.category === 'ui' && store.repairRounds >= 2) {
169
+ throw new AppError('worker_repair_limit', '本 UI task 已完成两轮实际返修;保留未通过问题并停止');
170
+ }
171
+ for (const item of parsed) {
172
+ if (item.advisory && packet.task?.category !== 'ui')
173
+ throw new AppError('review_advisory_ui_only', 'advisory 仅用于 UI 普通视觉偏差,不用于行为、构建或证据不可用');
174
+ if (item.status === 'resolved' && !store.ledger.issues[item.id])
175
+ throw new AppError('unknown_review_issue', `先登记问题再解决: ${item.id}`);
176
+ if (item.status === 'resolved' || item.advisory) {
177
+ if (!item.evidence)
178
+ throw new AppError('review_evidence_missing', `解决或披露问题需要证据: ${item.id}`);
179
+ const absolute = resolveInside(root, item.evidence, 'worker_evidence_outside_project');
180
+ if (!exists(absolute) || !statSync(absolute).isFile() || statSync(absolute).size === 0)
181
+ throw new AppError('review_evidence_missing', `证据必须是非空文件: ${item.id}`);
182
+ store.ledger.issues[item.id] = { ...item, evidenceDigest: sha256File(absolute), fingerprint: current };
183
+ }
184
+ else
185
+ store.ledger.issues[item.id] = item;
186
+ }
187
+ writeJson(resolveInside(root, `${store.directory}/review-${randomUUID()}.json`), {
188
+ at: new Date().toISOString(), updates: parsed.map((item) => store.ledger.issues[item.id]), codeFingerprint: current, repair: actualRepair,
189
+ });
190
+ const latest = load(root, change, packet);
191
+ writeJson(latest.absolute, latest.ledger);
192
+ appendEvent(root, change, 'worker.review', { workerId: packet.workerId, updates: parsed });
193
+ return {
194
+ path: store.relative,
195
+ open: Object.values(latest.ledger.issues).filter((item) => item.status === 'open').map((item) => item.id),
196
+ blocking: Object.values(latest.ledger.issues).filter((item) => item.status === 'open' && !item.advisory).map((item) => item.id),
197
+ advisory: Object.values(latest.ledger.issues).filter((item) => item.status === 'open' && item.advisory).map((item) => item.id),
198
+ rounds: latest.rounds, repairRounds: latest.repairRounds,
199
+ };
200
+ }
201
+ export function validateWorkerEvidence(root, change, packet) {
202
+ if (!packet.executionEvidence)
203
+ return; // Existing in-flight packets keep their original contract.
204
+ const store = load(root, change, packet);
205
+ const { ledger } = store;
206
+ writeJson(store.absolute, ledger);
207
+ const failures = [];
208
+ const fail = (code, message, details) => failures.push({ code, message, ...(details === undefined ? {} : { details }) });
209
+ const required = packet.initContext?.hooks.filter((item) => item.type === 'command' && item.required) ?? [];
210
+ for (const hook of required) {
211
+ const run = ledger.runs[hook.id];
212
+ if (!run || hook.type !== 'command' || run.command !== hook.command || ((packet.executionEvidenceVersion || run.workerId) && run.workerId !== packet.workerId) || (hook.plugin && (!run.plugin || JSON.stringify(run.plugin.binding) !== JSON.stringify(hook.plugin) || run.plugin.workerId !== packet.workerId || run.plugin.stepId !== packet.stepId)))
213
+ fail('worker_check_missing', `缺少 CLI 检查回执: ${hook.id}`);
214
+ }
215
+ const runs = Object.values(ledger.runs);
216
+ const current = runs.length || Object.keys(ledger.issues).length ? fingerprint(root) : '';
217
+ for (const run of runs) {
218
+ const optional = packet.initContext?.hooks.some((hook) => hook.id === run.id && hook.type === 'command' && !hook.required);
219
+ if (optional)
220
+ continue;
221
+ if (!run.ok)
222
+ fail('worker_check_failed', `检查失败,修复后用同一 id 重跑: ${run.id}`, { log: run.log });
223
+ else if ((packet.executionEvidenceVersion && run.workerId !== packet.workerId) || run.fingerprint !== current || !validArtifact(root, run.log, run.logDigest) || (run.plugin && !pluginEvidenceCurrent(root, run.plugin)) || (run.context && !commandContextCurrent(root, run.context)))
224
+ fail('worker_check_stale', `Worker、代码或日志已变化,重新检查: ${run.id}`);
225
+ }
226
+ for (const issue of Object.values(ledger.issues)) {
227
+ if (issue.status === 'open' && (!issue.advisory || packet.task?.category !== 'ui'))
228
+ fail('worker_review_open', `未解决的返修问题: ${issue.id}`, { description: issue.description });
229
+ else if (issue.fingerprint !== current || !issue.evidence || !issue.evidenceDigest || !validArtifact(root, issue.evidence, issue.evidenceDigest))
230
+ fail('worker_review_stale', `返修证据已变化,重新复核: ${issue.id}`);
231
+ }
232
+ if (failures.length)
233
+ throw new AppError(failures[0].code, failures.map((item) => item.message).join('\n'), { failures });
234
+ }
235
+ /** Final disclosure comes from review records, not optional prose in a worker result. */
236
+ export const VisualAdvisorySchema = Issue.extend({ stepId: z.string() });
237
+ export function visualAdvisories(root, change) {
238
+ const state = requireChange(root, change);
239
+ const directory = resolveInside(root, artifactRel(change, `.opsx/execution/r${state.sourceRevision}`));
240
+ if (!exists(directory))
241
+ return [];
242
+ return readdirSync(directory).filter(name => /^code-/.test(name) && statSync(path.join(directory, name)).isDirectory()).flatMap(stepId => {
243
+ const issues = {};
244
+ const reviews = readdirSync(path.join(directory, stepId)).filter(name => REVIEW_FILE.test(name))
245
+ .map(name => ({ name, record: ReviewRecord.parse(readJson(path.join(directory, stepId, name))) }))
246
+ .sort((a, b) => a.record.at.localeCompare(b.record.at) || a.name.localeCompare(b.name));
247
+ for (const { record } of reviews)
248
+ for (const issue of record.updates)
249
+ issues[issue.id] = issue;
250
+ return Object.values(issues).filter(issue => issue.status === 'open' && issue.advisory).map(issue => ({ ...issue, stepId }));
251
+ });
252
+ }
253
+ //# sourceMappingURL=worker-evidence.js.map
@@ -0,0 +1,22 @@
1
+ import { type ProjectConfig } from '../config/schema.js';
2
+ import { type ChangeState, type WorkerPacket } from './types.js';
3
+ export declare function createWorker(projectRoot: string, changeName: string, stepId: string, config: ProjectConfig, options?: {
4
+ refresh?: boolean;
5
+ }): {
6
+ packet: WorkerPacket;
7
+ path: string;
8
+ };
9
+ export declare function acceptWorker(projectRoot: string, changeName: string, resultPath: string, config: ProjectConfig): ChangeState;
10
+ /**
11
+ * 记录用户对 blocked Worker 的答复:写入 packet.decisions 并清除 blocked issue。
12
+ * 之后 `worker create` 同一 step 会复用该 packet,Worker 按 decisions 完成产物后重新 accept。
13
+ */
14
+ export declare function answerWorker(projectRoot: string, changeName: string, answers: string[]): {
15
+ state: ChangeState;
16
+ packet: WorkerPacket;
17
+ path: string;
18
+ };
19
+ export declare function cancelWorker(projectRoot: string, changeName: string, reason: string): ChangeState;
20
+ /** scout 子代理写出的 task 摘要路径;按 taskId 固定,与 workerId 无关,返修与重派都能复用。 */
21
+ export declare function taskContextRel(changeName: string, taskId: string): string;
22
+ //# sourceMappingURL=worker.d.ts.map