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.
- package/LICENSE +21 -0
- package/README.md +877 -0
- package/assets/agent/backend-builder.md +73 -0
- package/assets/agent/codebase-researcher.md +56 -0
- package/assets/agent/design-interpreter.md +37 -0
- package/assets/agent/frontend-builder.md +74 -0
- package/assets/agent/implementation-validator.md +73 -0
- package/assets/agent/security-reviewer.md +60 -0
- package/assets/agent/spec-writer.md +94 -0
- package/assets/agent/story-reader.md +38 -0
- package/assets/agent/story-writer.md +39 -0
- package/assets/agent/test-verifier.md +73 -0
- package/assets/agent/work-decomposer.md +102 -0
- package/assets/agent/work-reviewer.md +77 -0
- package/assets/command/feature.md +68 -0
- package/assets/skills/feature/SCHEMA.md +990 -0
- package/assets/skills/feature/SKILL.md +620 -0
- package/dist/tui.js +3641 -0
- package/package.json +65 -0
- package/src/cleanup-sweep-command.js +75 -0
- package/src/cleanup-sweep-eligibility.js +581 -0
- package/src/cleanup-sweep-output.js +139 -0
- package/src/cleanup-sweep-report.js +548 -0
- package/src/cleanup-sweep.js +546 -0
- package/src/cli-output.js +251 -0
- package/src/cli.js +1152 -0
- package/src/config.js +231 -0
- package/src/cost-attribution.js +327 -0
- package/src/cost-report.js +185 -0
- package/src/detached-log-supervisor.js +178 -0
- package/src/doctor.js +598 -0
- package/src/env-snapshot.js +211 -0
- package/src/factory-diagnostics.js +429 -0
- package/src/factory-paths.js +40 -0
- package/src/factory.js +4769 -0
- package/src/feature-command-payload.js +378 -0
- package/src/git.js +110 -0
- package/src/github.js +252 -0
- package/src/hardening/atomic-write.js +954 -0
- package/src/hardening/line-output.js +139 -0
- package/src/hardening/output-policy.js +365 -0
- package/src/hardening/process-verification.js +542 -0
- package/src/hardening/sensitive-data.js +341 -0
- package/src/hardening/terminal-encoding.js +224 -0
- package/src/plugin.js +246 -0
- package/src/post-pr-ci.js +754 -0
- package/src/process-evidence.js +1139 -0
- package/src/refs.js +144 -0
- package/src/run-state.js +2411 -0
- package/src/steering-conflicts.js +77 -0
- package/src/telemetry.js +419 -0
- package/src/tui-data.js +499 -0
- package/src/tui-rendering.js +148 -0
- package/src/utils.js +35 -0
- package/src/validate.js +1655 -0
- 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
|
+
}
|