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,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLEANUP_SWEEP_REFUSAL_REGISTRY,
|
|
3
|
+
createCleanupSweepReport,
|
|
4
|
+
parseCleanupSweepDigest,
|
|
5
|
+
} from "./cleanup-sweep-report.js";
|
|
6
|
+
import {
|
|
7
|
+
freeformSegment,
|
|
8
|
+
identitySegment,
|
|
9
|
+
projectFreeformData,
|
|
10
|
+
renderTerminalSegments,
|
|
11
|
+
TRUSTED_SEGMENTS,
|
|
12
|
+
} from "./hardening/output-policy.js";
|
|
13
|
+
import { serializeTerminalJson } from "./hardening/terminal-encoding.js";
|
|
14
|
+
|
|
15
|
+
const CONFIRMATION_PREFIX = Object.freeze(["feature-factory", "factory", "cleanup", "--all", "--digest"]);
|
|
16
|
+
|
|
17
|
+
export function createCleanupSweepConfirmation(digest, repositoryRoot, options = {}) {
|
|
18
|
+
parseCleanupSweepDigest(digest);
|
|
19
|
+
if (typeof repositoryRoot !== "string" || repositoryRoot.length === 0) {
|
|
20
|
+
throw new TypeError("cleanup sweep confirmation requires a physical repository root");
|
|
21
|
+
}
|
|
22
|
+
const argv = [...CONFIRMATION_PREFIX, digest, "--repo", repositoryRoot];
|
|
23
|
+
if (options.json === true) argv.push("--json");
|
|
24
|
+
return { argv, shell_command: shellConfirmationCommand(argv, repositoryRoot) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function shellQuoteArgv(argv) {
|
|
28
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.some((arg) => typeof arg !== "string")) {
|
|
29
|
+
throw new TypeError("shell argv must be a non-empty array of strings");
|
|
30
|
+
}
|
|
31
|
+
return argv.map((arg) => `'${arg.replaceAll("'", `'"'"'`)}'`).join(" ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderCleanupSweepReport(report, options = {}) {
|
|
35
|
+
return renderCleanupSweepReportLines(report, options).join("\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderCleanupSweepReportLines(report, options = {}) {
|
|
39
|
+
const normalized = createCleanupSweepReport(report);
|
|
40
|
+
const confirmation = exactReportConfirmation(normalized);
|
|
41
|
+
if (options.json === true) {
|
|
42
|
+
return [serializeTerminalJson(normalized, { space: 2 })];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lines = [
|
|
46
|
+
keyValueLine("mode", identitySegment(normalized.mode)),
|
|
47
|
+
keyValueLine("status", identitySegment(normalized.status)),
|
|
48
|
+
keyValueLine("repository", normalized.repository === null
|
|
49
|
+
? identitySegment("-")
|
|
50
|
+
: freeformSegment(normalized.repository.root_path)),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
if (normalized.authorization.digest !== null) {
|
|
54
|
+
lines.push(keyValueLine("digest", identitySegment(normalized.authorization.digest)));
|
|
55
|
+
}
|
|
56
|
+
if (normalized.authorization.refusal_code !== null) {
|
|
57
|
+
const refusal = CLEANUP_SWEEP_REFUSAL_REGISTRY[normalized.authorization.refusal_code];
|
|
58
|
+
lines.push(renderTerminalSegments([
|
|
59
|
+
identitySegment("refusal"), TRUSTED_SEGMENTS.COLON_SPACE,
|
|
60
|
+
identitySegment(refusal.code), TRUSTED_SEGMENTS.DASH_SEPARATOR, freeformSegment(refusal.message),
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
for (const error of normalized.report_errors) {
|
|
64
|
+
lines.push(renderTerminalSegments([
|
|
65
|
+
identitySegment("report-error"), TRUSTED_SEGMENTS.COLON_SPACE,
|
|
66
|
+
identitySegment(error.code), TRUSTED_SEGMENTS.DASH_SEPARATOR, freeformSegment(error.message),
|
|
67
|
+
]));
|
|
68
|
+
}
|
|
69
|
+
for (const candidate of normalized.candidates) lines.push(candidateLine(candidate));
|
|
70
|
+
|
|
71
|
+
lines.push(keyValueLine("counts", identitySegment([
|
|
72
|
+
`eligible=${normalized.counts.eligible}`,
|
|
73
|
+
`protected=${normalized.counts.protected}`,
|
|
74
|
+
`skipped=${normalized.counts.skipped}`,
|
|
75
|
+
`deleted=${normalized.counts.deleted}`,
|
|
76
|
+
`failed=${normalized.counts.failed}`,
|
|
77
|
+
].join(" "))));
|
|
78
|
+
lines.push(keyValueLine("attempted-cleanup-failures", identitySegment(normalized.attempted_cleanup_failures)));
|
|
79
|
+
if (confirmation !== null) {
|
|
80
|
+
// This command is constructed above from fixed ASCII framing and either
|
|
81
|
+
// shell-quoted terminal-safe text or octal UTF-8 bytes. Re-encoding it as a
|
|
82
|
+
// diagnostic identity would change the executable backslashes.
|
|
83
|
+
lines.push(`confirmation: ${confirmation.shell_command}`);
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function exactReportConfirmation(report) {
|
|
89
|
+
if (report.confirmation.argv === null) return null;
|
|
90
|
+
const hasJson = report.confirmation.argv.at(-1) === "--json";
|
|
91
|
+
const expected = createCleanupSweepConfirmation(
|
|
92
|
+
report.authorization.digest,
|
|
93
|
+
report.repository.root_path,
|
|
94
|
+
{ json: hasJson },
|
|
95
|
+
);
|
|
96
|
+
if (JSON.stringify(report.confirmation) !== JSON.stringify(expected)) {
|
|
97
|
+
throw new TypeError("cleanup sweep preview confirmation does not match the report authorization");
|
|
98
|
+
}
|
|
99
|
+
return expected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function candidateLine(candidate) {
|
|
103
|
+
return renderTerminalSegments([
|
|
104
|
+
identitySegment(candidate.classification), TRUSTED_SEGMENTS.TAB,
|
|
105
|
+
freeformSegment(candidate.entry_name), TRUSTED_SEGMENTS.TAB,
|
|
106
|
+
identitySegment(candidate.reason_codes.join(",")), TRUSTED_SEGMENTS.TAB,
|
|
107
|
+
freeformSegment(candidate.reasons.map((reason) => reason.message).join(" | ")),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function keyValueLine(key, valueSegment) {
|
|
112
|
+
return renderTerminalSegments([identitySegment(key), TRUSTED_SEGMENTS.COLON_SPACE, valueSegment]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function shellConfirmationCommand(argv, repositoryRoot) {
|
|
116
|
+
assertRoundTrippablePath(repositoryRoot);
|
|
117
|
+
if (projectFreeformData(repositoryRoot) === repositoryRoot && isAsciiTerminalSafe(repositoryRoot)) {
|
|
118
|
+
return shellQuoteArgv(argv);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const repoIndex = argv.indexOf("--repo") + 1;
|
|
122
|
+
const beforeRepository = shellQuoteArgv(argv.slice(0, repoIndex));
|
|
123
|
+
const afterRepository = argv.slice(repoIndex + 1);
|
|
124
|
+
const suffix = afterRepository.length === 0 ? "" : ` ${shellQuoteArgv(afterRepository)}`;
|
|
125
|
+
const octalBytes = [...Buffer.from(repositoryRoot, "utf8")]
|
|
126
|
+
.map((byte) => `\\${byte.toString(8).padStart(3, "0")}`)
|
|
127
|
+
.join("");
|
|
128
|
+
return `_ff_cleanup_repo=$(printf '%b_' '${octalBytes}'); _ff_cleanup_repo=\${_ff_cleanup_repo%_}; ${beforeRepository} \"$_ff_cleanup_repo\"${suffix}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function assertRoundTrippablePath(value) {
|
|
132
|
+
if (value.includes("\0") || Buffer.from(value, "utf8").toString("utf8") !== value) {
|
|
133
|
+
throw new TypeError("cleanup sweep confirmation requires a round-trippable physical repository root");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isAsciiTerminalSafe(value) {
|
|
138
|
+
return /^[\x20-\x7e]+$/u.test(value);
|
|
139
|
+
}
|