opencode-feature-factory 0.2.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +877 -0
  3. package/assets/agent/backend-builder.md +73 -0
  4. package/assets/agent/codebase-researcher.md +56 -0
  5. package/assets/agent/design-interpreter.md +37 -0
  6. package/assets/agent/frontend-builder.md +74 -0
  7. package/assets/agent/implementation-validator.md +73 -0
  8. package/assets/agent/security-reviewer.md +60 -0
  9. package/assets/agent/spec-writer.md +94 -0
  10. package/assets/agent/story-reader.md +38 -0
  11. package/assets/agent/story-writer.md +39 -0
  12. package/assets/agent/test-verifier.md +73 -0
  13. package/assets/agent/work-decomposer.md +102 -0
  14. package/assets/agent/work-reviewer.md +77 -0
  15. package/assets/command/feature.md +68 -0
  16. package/assets/skills/feature/SCHEMA.md +990 -0
  17. package/assets/skills/feature/SKILL.md +620 -0
  18. package/dist/tui.js +3641 -0
  19. package/package.json +65 -0
  20. package/src/cleanup-sweep-command.js +75 -0
  21. package/src/cleanup-sweep-eligibility.js +581 -0
  22. package/src/cleanup-sweep-output.js +139 -0
  23. package/src/cleanup-sweep-report.js +548 -0
  24. package/src/cleanup-sweep.js +546 -0
  25. package/src/cli-output.js +251 -0
  26. package/src/cli.js +1152 -0
  27. package/src/config.js +231 -0
  28. package/src/cost-attribution.js +327 -0
  29. package/src/cost-report.js +185 -0
  30. package/src/detached-log-supervisor.js +178 -0
  31. package/src/doctor.js +598 -0
  32. package/src/env-snapshot.js +211 -0
  33. package/src/factory-diagnostics.js +429 -0
  34. package/src/factory-paths.js +40 -0
  35. package/src/factory.js +4769 -0
  36. package/src/feature-command-payload.js +378 -0
  37. package/src/git.js +110 -0
  38. package/src/github.js +252 -0
  39. package/src/hardening/atomic-write.js +954 -0
  40. package/src/hardening/line-output.js +139 -0
  41. package/src/hardening/output-policy.js +365 -0
  42. package/src/hardening/process-verification.js +542 -0
  43. package/src/hardening/sensitive-data.js +341 -0
  44. package/src/hardening/terminal-encoding.js +224 -0
  45. package/src/plugin.js +246 -0
  46. package/src/post-pr-ci.js +754 -0
  47. package/src/process-evidence.js +1139 -0
  48. package/src/refs.js +144 -0
  49. package/src/run-state.js +2411 -0
  50. package/src/steering-conflicts.js +77 -0
  51. package/src/telemetry.js +419 -0
  52. package/src/tui-data.js +499 -0
  53. package/src/tui-rendering.js +148 -0
  54. package/src/utils.js +35 -0
  55. package/src/validate.js +1655 -0
  56. package/src/worktrees.js +56 -0
@@ -0,0 +1,251 @@
1
+ import {
2
+ freeformSegment,
3
+ identitySegment,
4
+ projectFreeformData,
5
+ renderTerminalSegments,
6
+ TRUSTED_SEGMENTS,
7
+ } from "./hardening/output-policy.js";
8
+ import { REDACTED_VALUE, isSensitiveValue, scrubSensitiveString } from "./hardening/sensitive-data.js";
9
+ import { serializeTerminalJson } from "./hardening/terminal-encoding.js";
10
+
11
+ const VALIDATED_STATUS_VALUES = new Set([
12
+ "accepted", "approved", "available", "blocked", "cancelled", "changes_requested",
13
+ "completed", "failed", "indeterminate", "invalid", "live", "missing", "needs-human",
14
+ "ok", "partial", "pending", "ready", "rejected", "review", "running", "stopped",
15
+ "unavailable", "warn", "warning",
16
+ ]);
17
+ const SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
18
+ const SAFE_UUID_PATTERN = /^[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}$/u;
19
+ const SAFE_HASH_PATTERN = /^(?:sha256:)?[A-Fa-f0-9]{32,128}$/u;
20
+ const SAFE_REF_CHARACTERS = /^[A-Za-z0-9._/-]+$/u;
21
+ const SAFE_COST_COLUMN_PATTERN = /^cost (?:available|partial|unavailable) · [0-9]+ (?:entry|entries)(?: · (?:[0-9?]+(?:\/[0-9?]+)? tokens|mixed currency|[0-9]+(?:\.[0-9]{1,6})? [A-Z]{3,12}|missing [A-Za-z0-9_, -]+))*$/u;
22
+
23
+ export function printCliResult(value, options = {}, helpers = {}) {
24
+ const write = helpers.write || console.log;
25
+ for (const line of renderCliResultLines(value, options, helpers)) write(line);
26
+ }
27
+
28
+ export function renderCliResultLines(value, options = {}, helpers = {}) {
29
+ if (value === undefined) return [];
30
+ if (value === null || options.json || typeof value !== "object") {
31
+ return [typeof value === "string"
32
+ ? renderTerminalSegments([freeformSegment(value)])
33
+ : serializeTerminalJson(projectCliData(value), { space: 2 })];
34
+ }
35
+ if (Array.isArray(value)) return value.map((item) => renderListRow(item, helpers));
36
+ return Object.entries(value).map(([key, entry]) => renderKeyValueRow(key, entry));
37
+ }
38
+
39
+ export function projectCliData(value) {
40
+ return projectCliValue(value, null);
41
+ }
42
+
43
+ function projectCliValue(value, key) {
44
+ if (typeof value === "string") {
45
+ if (validatedIdentity(key, value)) return value;
46
+ return contractualIdentityKey(key) ? REDACTED_VALUE : projectFreeformData(value);
47
+ }
48
+ if (value === null || typeof value !== "object") return value;
49
+ if (Array.isArray(value)) return value.map((entry) => projectCliValue(entry, null));
50
+ const projected = Object.create(null);
51
+ for (const [key, entry] of Object.entries(value)) {
52
+ defineEntry(projected, key, projectCliValue(entry, key));
53
+ }
54
+ return projected;
55
+ }
56
+
57
+ export function projectCostReport(report) {
58
+ const projected = projectCliData(report);
59
+ for (const field of ["by_agent", "by_step", "by_slice"]) {
60
+ if (!projected?.[field] || typeof projected[field] !== "object") continue;
61
+ projected[field] = projectDynamicKeys(projected[field]);
62
+ }
63
+ return projected;
64
+ }
65
+
66
+ export function renderCliFreeform(value) {
67
+ return renderTerminalSegments([freeformSegment(value)]);
68
+ }
69
+
70
+ export function renderCliPath(value) {
71
+ return renderTerminalSegments([identitySegment(projectPath(value))]);
72
+ }
73
+
74
+ function renderListRow(item, helpers) {
75
+ const cost = typeof helpers.formatListCostColumn === "function"
76
+ ? helpers.formatListCostColumn(item)
77
+ : "-";
78
+ return renderTerminalSegments([
79
+ projectedTableSegment("run_id", item?.run_id), TRUSTED_SEGMENTS.TAB,
80
+ projectedTableSegment("status", item?.status), TRUSTED_SEGMENTS.TAB,
81
+ projectedTableSegment("gate", item?.gate || "-"), TRUSTED_SEGMENTS.TAB,
82
+ projectedTableSegment("updated_at", item?.updated_at || "-"), TRUSTED_SEGMENTS.TAB,
83
+ ...projectedTableSegments("cost", cost), TRUSTED_SEGMENTS.TAB,
84
+ ...diagnosticColumnSegments(item?.diagnostics, helpers),
85
+ ]);
86
+ }
87
+
88
+ function diagnosticColumnSegments(diagnostics, helpers) {
89
+ if (!diagnostics || typeof diagnostics !== "object") return [identitySegment("-")];
90
+ if (diagnostics.status === "ok") return [identitySegment("ok")];
91
+ const prefix = [diagnostics.classification, diagnostics.status].filter(stringValue).join("/") || "diagnostic";
92
+ const rawSummary = diagnostics.summary || "check diagnostics";
93
+ const summary = typeof helpers.cleanDiagnosticText === "function"
94
+ ? helpers.cleanDiagnosticText(rawSummary)
95
+ : rawSummary;
96
+ return [freeformSegment(`${prefix}:`), freeformSegment(summary)];
97
+ }
98
+
99
+ function renderKeyValueRow(key, value) {
100
+ const renderedValue = value !== null && typeof value === "object"
101
+ ? identitySegment(serializeTerminalJson(projectCliData(value)))
102
+ : projectedValueSegment(key, value);
103
+ return renderTerminalSegments([identitySegment(key), TRUSTED_SEGMENTS.COLON_SPACE, renderedValue]);
104
+ }
105
+
106
+ function projectDynamicKeys(value) {
107
+ const output = Object.create(null);
108
+ for (const [key, entry] of Object.entries(value)) {
109
+ let projectedKey = scrubSensitiveString(key, { mode: "baseline" });
110
+ for (let suffix = 2; Object.hasOwn(output, projectedKey); suffix += 1) {
111
+ projectedKey = `${scrubSensitiveString(key, { mode: "baseline" })}#${suffix}`;
112
+ }
113
+ defineEntry(output, projectedKey, entry);
114
+ }
115
+ return output;
116
+ }
117
+
118
+ function stringValue(value) {
119
+ return typeof value === "string" && value.trim().length > 0;
120
+ }
121
+
122
+ function projectedTableSegments(key, value) {
123
+ if (key === "cost" && (value === "-" || SAFE_COST_COLUMN_PATTERN.test(String(value)))) {
124
+ const parts = String(value).split(" · ");
125
+ return parts.flatMap((part, index) => index === 0
126
+ ? [identitySegment(part)]
127
+ : [freeformSegment(" · "), identitySegment(part)]);
128
+ }
129
+ return [projectedTableSegment(key, value)];
130
+ }
131
+
132
+ function projectedTableSegment(key, value) {
133
+ return projectedValueSegment(key, value);
134
+ }
135
+
136
+ function projectedValueSegment(key, value) {
137
+ if (validatedIdentity(key, value)) return identitySegment(value);
138
+ return freeformSegment(contractualIdentityKey(key) ? REDACTED_VALUE : value);
139
+ }
140
+
141
+ function validatedIdentity(key, value) {
142
+ if (typeof value !== "string") return false;
143
+ if (key === "status" || key === "level") return VALIDATED_STATUS_VALUES.has(value);
144
+ if (key === "run_id") return !isSensitiveValue(value, { mode: "baseline" }) && SAFE_RUN_ID_PATTERN.test(value);
145
+ if (key === "token" || key === "boundary_token" || key === "action_token" || key === "fence_token") {
146
+ // Factory-issued tokens have exactly one shape: the UUID minted at
147
+ // boundary/action/fence creation (run-state.js randomUUID()); operators only
148
+ // ever echo factory-issued tokens back. Render raw only that closed shape
149
+ // and redact everything else deterministically. This deliberately replaces
150
+ // credential classification for token fields: no blocklist heuristic can
151
+ // separate a provider credential from a descriptive string that mentions
152
+ // credential words, and chasing that precision/recall boundary is what
153
+ // exhausted the autonomous run that produced this slice.
154
+ return SAFE_UUID_PATTERN.test(value);
155
+ }
156
+ if (key === "hash" || key === "trace_id" || key?.endsWith("_hash")) return SAFE_HASH_PATTERN.test(value);
157
+ if (key === "ref" || key?.endsWith("_ref")) return safeContractRef(value, { allowRunRoot: key === "run_ref" });
158
+ if (key === "branch") return safeContractRef(value);
159
+ if (key === "commit" || key?.endsWith("_commit")) return /^[A-Fa-f0-9]{7,64}$/u.test(value);
160
+ if (key === "worktree" || key === "path" || key?.endsWith("_path")) {
161
+ return safeContractPath(value);
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function contractualIdentityKey(key) {
167
+ return key === "run_id"
168
+ || key === "token"
169
+ || key === "boundary_token"
170
+ || key === "action_token"
171
+ || key === "fence_token"
172
+ || key === "hash"
173
+ || key === "trace_id"
174
+ || key?.endsWith("_hash")
175
+ || key === "ref"
176
+ || key?.endsWith("_ref")
177
+ || key === "branch"
178
+ || key === "commit"
179
+ || key?.endsWith("_commit")
180
+ || key === "worktree"
181
+ || key === "path"
182
+ || key?.endsWith("_path");
183
+ }
184
+
185
+ function safeContractRef(value, { allowRunRoot = false } = {}) {
186
+ if (!value) return false;
187
+ const segments = value.split("/");
188
+ if (segments.some(centrallySensitiveStructuredSegment) || !SAFE_REF_CHARACTERS.test(value)) return false;
189
+ if (value.startsWith("-") || value.startsWith("/") || value.endsWith("/") || value.endsWith(".")) return false;
190
+ if (value === "@" || value.includes("//") || value.includes("..") || value.includes("@{")) return false;
191
+ return segments.every((segment, index) => {
192
+ if (!segment || segment.endsWith(".") || segment.toLowerCase().endsWith(".lock")) return false;
193
+ if (!segment.startsWith(".")) return true;
194
+ return allowRunRoot && index === 0 && segment === ".opencode";
195
+ });
196
+ }
197
+
198
+ function safeContractPath(value) {
199
+ if (!value || hasUnsafeTerminalCodePoint(value)) return false;
200
+ // Evaluate the complete value so credentials spanning path separators remain
201
+ // visible to the centralized policy. The marker prevents its generic
202
+ // single-token entropy heuristic from treating an ordinary long absolute
203
+ // path as opaque secret material; credential/header/URL recognizers still
204
+ // match the unchanged value before the marker.
205
+ if (isSensitiveValue(`${value}#`, { mode: "baseline" })) return false;
206
+ return value.split(/[\\/]/u).every((segment) => !centrallySensitiveStructuredSegment(segment));
207
+ }
208
+
209
+ function centrallySensitiveStructuredSegment(segment) {
210
+ if (!segment || !isSensitiveValue(segment, { mode: "baseline" })) return false;
211
+ const scrubbed = scrubSensitiveString(segment, { mode: "baseline" });
212
+ if (scrubbed !== REDACTED_VALUE) return true;
213
+ if (segment.split(/[-_.]+/u).some((part) => part && isSensitiveValue(part, { mode: "baseline" }))) return true;
214
+ // Central token/PAT recognizers match in windows shorter than the generic
215
+ // high-entropy threshold. This preserves benign generated worktree directory
216
+ // names while still rejecting a credential embedded anywhere in a segment.
217
+ for (let start = 0; start < segment.length; start += 1) {
218
+ if (isSensitiveValue(segment.slice(start, start + 31), { mode: "baseline" })) return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ function hasUnsafeTerminalCodePoint(value) {
224
+ for (const character of value) {
225
+ const codePoint = character.codePointAt(0);
226
+ if (codePoint <= 0x1f
227
+ || (codePoint >= 0x7f && codePoint <= 0x9f)
228
+ || (codePoint >= 0x202a && codePoint <= 0x202e)
229
+ || (codePoint >= 0x2066 && codePoint <= 0x2069)) return true;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ function projectPath(value) {
235
+ const text = String(value ?? "");
236
+ const projected = projectFreeformData(text);
237
+ if (projected !== REDACTED_VALUE) return projected;
238
+ return text
239
+ .split(/([/\\\s-]+)/u)
240
+ .map((part) => /^(?:[/\\\s-]+)$/u.test(part) ? part : projectFreeformData(part))
241
+ .join("");
242
+ }
243
+
244
+ function defineEntry(target, key, value) {
245
+ Object.defineProperty(target, key, {
246
+ value,
247
+ enumerable: true,
248
+ configurable: true,
249
+ writable: true,
250
+ });
251
+ }