opencode-feature-factory 0.2.0 → 0.2.1
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 +4 -4
- package/dist/tui.js +42 -4
- package/package.json +1 -1
- package/src/cleanup-sweep-eligibility.js +29 -10
- package/src/cleanup-sweep-output.js +6 -1
- package/src/cleanup-sweep.js +16 -63
- package/src/cli-output.js +2 -2
- package/src/hardening/output-policy.js +14 -0
- package/src/hardening/sensitive-data.js +28 -1
- package/src/tui-data.js +4 -2
- package/src/tui-rendering.js +8 -1
package/README.md
CHANGED
|
@@ -98,9 +98,9 @@ The published package supports Node `>=20`. Repository tooling selects Node `24.
|
|
|
98
98
|
|
|
99
99
|
### Release workflow
|
|
100
100
|
|
|
101
|
-
Publication is tag-driven only. Pushing `v<package.json version>` (for package version `0.2.
|
|
101
|
+
Publication is tag-driven only. Pushing `v<package.json version>` (for package version `0.2.1`, `v0.2.1`) triggers the workflow, which checks out that pushed tag, selects Node 24, runs `npm ci`, enforces exact equality between the tag and `v${package.json.version}`, runs `npm run check`, and then runs `npm publish`. The publish job uses the `npm` environment and `id-token: write` for trusted publication.
|
|
102
102
|
|
|
103
|
-
The workflow does not publish from a branch or manual dispatch, and it does not bump the version, create or push the tag, generate a changelog, push commits, or create a GitHub Release. Operators own those preparatory actions. See [RELEASING.md](https://github.com/jasoncarreira/opencode-feature-factory/blob/main/RELEASING.md) for the repository release guide and [CHANGELOG.md](https://github.com/jasoncarreira/opencode-feature-factory/blob/main/CHANGELOG.md) for the verified
|
|
103
|
+
The workflow does not publish from a branch or manual dispatch, and it does not bump the version, create or push the tag, generate a changelog, push commits, or create a GitHub Release. Operators own those preparatory actions. See [RELEASING.md](https://github.com/jasoncarreira/opencode-feature-factory/blob/main/RELEASING.md) for the repository release guide and [CHANGELOG.md](https://github.com/jasoncarreira/opencode-feature-factory/blob/main/CHANGELOG.md) for the verified release records.
|
|
104
104
|
|
|
105
105
|
## Local Diagnostics
|
|
106
106
|
|
|
@@ -553,7 +553,7 @@ The preview emits a repository- and evidence-bound digest plus an exact confirma
|
|
|
553
553
|
feature-factory factory cleanup --all --digest ff-cleanup-v1.<repository-sha256>.<envelope-sha256> [--repo PATH] [--json]
|
|
554
554
|
```
|
|
555
555
|
|
|
556
|
-
The sweep never accepts a positional run id, `--force`, or unrelated options. It deletes only runs whose status is exactly `completed` and whose current evidence positively proves all cleanup conditions: canonical PR metadata resolves to the exact merged or closed PR; every recorded local branch is contained in a freshly fetched trustworthy PR base; every recorded worktree and branch has safe containment and exact identity; and no active factory ownership, fresh heartbeat, identity-matching live process, run-state lock, shared target, or contradictory evidence remains. Open or unavailable PRs, unmerged or unprovable branches, malformed state, pre-manifest entries, unsafe paths, and any filesystem, Git, GitHub, lock, heartbeat, process, or sidecar uncertainty are skipped rather than deleted.
|
|
556
|
+
The sweep never accepts a positional run id, `--force`, or unrelated options. It deletes only runs whose status is exactly `completed` and whose current evidence positively proves all cleanup conditions: canonical PR metadata resolves to the exact merged or closed PR; every recorded local branch is contained in a freshly fetched trustworthy PR base at the current head of the canonical base branch; every recorded worktree and branch has safe containment and exact identity; and no active factory ownership, fresh heartbeat, identity-matching live process, run-state lock, shared target, or contradictory evidence remains. Open or unavailable PRs, unmerged or unprovable branches, malformed state, pre-manifest entries, unsafe paths, and any filesystem, Git, GitHub, lock, heartbeat, process, or sidecar uncertainty are skipped rather than deleted.
|
|
557
557
|
|
|
558
558
|
Execution first recomputes the complete digest and refuses a foreign or stale digest before attempting any candidate. It then acquires each candidate's run-state lock without reclaiming a contested lock and repeats the complete eligibility check while holding that lock. Evidence that changed during lock-held revalidation is skipped. The sweep never takes over a lock, force-deletes a branch, repairs a run, or broadens the recorded cleanup targets.
|
|
559
559
|
|
|
@@ -582,7 +582,7 @@ feature-factory factory cleanup --all --digest ff-cleanup-v1.<repository-sha256>
|
|
|
582
582
|
|
|
583
583
|
Sweep flags may appear in any order, but `--all` must appear exactly once and must select exactly one mode: `--dry-run` once for preview or `--digest VALUE` once for execution. `--repo PATH` and `--json` are each optional once. Sweep cleanup accepts no run ID, `--force`, repeated flag, unrelated option, short option, `--flag=value`, or `--` separator. A digest has exactly two 64-character lowercase hexadecimal components. Grammar errors produce one terminal-safe `error:` line on stderr, no report on stdout, and perform no repository inspection.
|
|
584
584
|
|
|
585
|
-
Sweep cleanup is deliberately stricter than single-run cleanup. A run is eligible only when its status is exactly `completed`, its canonical recorded GitHub PR is currently merged or closed, every recorded branch is proven contained in a freshly fetched PR base, every worktree and branch passes containment and identity checks, and no active ownership, fresh heartbeat, live identity-matching process, run lock, shared target, or contradictory evidence remains. Missing, malformed, inaccessible, stale, foreign, or otherwise unprovable evidence skips deletion; `--force` is not supported with `--all`.
|
|
585
|
+
Sweep cleanup is deliberately stricter than single-run cleanup. A run is eligible only when its status is exactly `completed`, its canonical recorded GitHub PR is currently merged or closed, every recorded branch is proven contained in a freshly fetched PR base at the current head of the canonical base branch, every worktree and branch passes containment and identity checks, and no active ownership, fresh heartbeat, live identity-matching process, run lock, shared target, or contradictory evidence remains. Missing, malformed, inaccessible, stale, foreign, or otherwise unprovable evidence skips deletion; `--force` is not supported with `--all`.
|
|
586
586
|
|
|
587
587
|
Execution first resolves `--repo` to its physical identity and recomputes the complete digest. The exact physical root, device/inode identity, Git common-directory identity, and object format bind authorization; display text never does. A digest from another repository is refused as `DIGEST_FOREIGN`; changed candidate evidence is refused as `DIGEST_STALE`. Both refusals preserve the recomputed unattempted candidates and counts, provide no confirmation action, and require a fresh preview. After a matching digest, each eligible candidate is locked without lock takeover and fully revalidated immediately before mutation. Changed or contested candidates are skipped. Deletion delegates to the existing identity-checked single-run target semantics, uses no branch force deletion, retains the run directory after a partial target failure, and continues processing independent candidates.
|
|
588
588
|
|
package/dist/tui.js
CHANGED
|
@@ -1846,6 +1846,19 @@ var TOKEN_SHAPED_VALUE_PATTERNS = Object.freeze([
|
|
|
1846
1846
|
/(?:^|[^A-Za-z0-9-])(?:[A-Fa-f0-9]{32,})(?=$|[^A-Za-z0-9-])/u,
|
|
1847
1847
|
/(?:https?|ssh|git|ftp):\/\/[^/\s:@]+:[^/\s@]+@/iu
|
|
1848
1848
|
]);
|
|
1849
|
+
var TOKEN_SHAPED_FRAGMENT_PATTERNS = Object.freeze([
|
|
1850
|
+
/gh[pousr]_[A-Za-z0-9_]{20,}/iu,
|
|
1851
|
+
/github_pat_[A-Za-z0-9_]{20,}/iu,
|
|
1852
|
+
/hc_[A-Za-z0-9][A-Za-z0-9_-]{10,}/iu,
|
|
1853
|
+
/sk-proj[-_][A-Za-z0-9_-]{20,}/iu,
|
|
1854
|
+
/sk-[A-Za-z0-9_-]{20,}/iu,
|
|
1855
|
+
/xox[abp][_-][A-Za-z0-9-]{10,}(?:-[A-Za-z0-9-]{10,})*/iu,
|
|
1856
|
+
/glpat-[A-Za-z0-9_-]{20,}/iu,
|
|
1857
|
+
/Bearer\s+[A-Za-z0-9._~+/=-]{20,}/iu,
|
|
1858
|
+
/eyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/iu,
|
|
1859
|
+
/(?:AKIA|ASIA)[A-Z0-9]{16}/u,
|
|
1860
|
+
/[A-Fa-f0-9]{32,}/u
|
|
1861
|
+
]);
|
|
1849
1862
|
var ENDPOINT_PROVIDER_PREFIX_PATTERN = /(?:hc[a-z0-9_-]*|gh[pousr]|github_pat|sk(?:-proj)?|xox[abp]|glpat)[_-][A-Za-z0-9_-]{10,}/iu;
|
|
1850
1863
|
var ENDPOINT_BARE_HEX_PATTERN = /^[A-Fa-f0-9]{32,}$/u;
|
|
1851
1864
|
var ENDPOINT_SECRET_ASSIGNMENT_PATTERN = /(?:(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key)\s*[=:]|[=:]\s*(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key))/iu;
|
|
@@ -1876,11 +1889,23 @@ function isSecretShapedKey(value, { mode = "baseline" } = {}) {
|
|
|
1876
1889
|
return tokenShapedValue(trimmed) || credentialBearingUrl2(trimmed) || highEntropySingleToken2(trimmed) || mode === "endpoint" && endpointValueLooksSensitive(trimmed);
|
|
1877
1890
|
}
|
|
1878
1891
|
function isSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
1892
|
+
return isRecognizedSensitiveValue(value, { mode }) || highEntropyValue(value);
|
|
1893
|
+
}
|
|
1894
|
+
function isRecognizedSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
1879
1895
|
assertMode(mode);
|
|
1880
1896
|
if (typeof value !== "string") return false;
|
|
1881
1897
|
const trimmed = value.trim();
|
|
1882
1898
|
if (!trimmed) return false;
|
|
1883
|
-
return FLATTENED_BASIC_HEADER_PATTERN.test(value) || BASELINE_SENSITIVE_VALUE_PATTERN.test(trimmed) || tokenShapedValue(trimmed) || credentialBearingUrl2(trimmed) ||
|
|
1899
|
+
return FLATTENED_BASIC_HEADER_PATTERN.test(value) || BASELINE_SENSITIVE_VALUE_PATTERN.test(trimmed) || tokenShapedValue(trimmed) || credentialBearingUrl2(trimmed) || mode === "endpoint" && endpointValueLooksSensitive(trimmed);
|
|
1900
|
+
}
|
|
1901
|
+
function containsRecognizedSensitiveFragment(value) {
|
|
1902
|
+
if (typeof value !== "string") return false;
|
|
1903
|
+
return TOKEN_SHAPED_FRAGMENT_PATTERNS.some((pattern) => pattern.test(value));
|
|
1904
|
+
}
|
|
1905
|
+
function highEntropyValue(value) {
|
|
1906
|
+
if (typeof value !== "string") return false;
|
|
1907
|
+
const trimmed = value.trim();
|
|
1908
|
+
return Boolean(trimmed && highEntropySingleToken2(trimmed));
|
|
1884
1909
|
}
|
|
1885
1910
|
function scrubSensitiveString(value, { mode = "baseline" } = {}) {
|
|
1886
1911
|
assertMode(mode);
|
|
@@ -2307,6 +2332,9 @@ var DIAGNOSTIC_FREEFORM_FIELDS = Object.freeze([
|
|
|
2307
2332
|
"action"
|
|
2308
2333
|
]);
|
|
2309
2334
|
var OUTPUT_POLICY_ERROR_CODE = "ERR_OUTPUT_POLICY";
|
|
2335
|
+
var SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
2336
|
+
var DESCRIPTIVE_RUN_ID_PATTERN = /^[a-z]{1,32}(?:-[a-z]{1,32})+(?:-[0-9]{1,6})?$/u;
|
|
2337
|
+
var UUID_PATTERN = /[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}/u;
|
|
2310
2338
|
var segmentBrand = /* @__PURE__ */ new WeakSet();
|
|
2311
2339
|
var TRUSTED_FRAMING_TEXT = Object.freeze({
|
|
2312
2340
|
EMPTY: "",
|
|
@@ -2365,6 +2393,12 @@ function projectFreeformData(value) {
|
|
|
2365
2393
|
fail2("projection");
|
|
2366
2394
|
}
|
|
2367
2395
|
}
|
|
2396
|
+
function isDisplaySafeRunId(value) {
|
|
2397
|
+
if (typeof value !== "string" || !SAFE_RUN_ID_PATTERN.test(value) || value.includes("..") || value.endsWith(".lock") || UUID_PATTERN.test(value)) return false;
|
|
2398
|
+
if (isRecognizedSensitiveValue(value, { mode: "baseline" }) || containsRecognizedSensitiveFragment(value)) return false;
|
|
2399
|
+
if (!isSensitiveValue(value, { mode: "baseline" })) return true;
|
|
2400
|
+
return DESCRIPTIVE_RUN_ID_PATTERN.test(value) && value.split("-").every((segment) => /^\d+$/u.test(segment) || !isSensitiveValue(segment, { mode: "baseline" }));
|
|
2401
|
+
}
|
|
2368
2402
|
function projectDiagnosticData(value, options = void 0) {
|
|
2369
2403
|
try {
|
|
2370
2404
|
const policy = readDiagnosticPolicy(options);
|
|
@@ -3104,7 +3138,7 @@ function parseErrorDiagnostics(file, error) {
|
|
|
3104
3138
|
}
|
|
3105
3139
|
function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
3106
3140
|
return {
|
|
3107
|
-
run_id:
|
|
3141
|
+
run_id: run.run_id === fallbackID && isDisplaySafeRunId(run.run_id) ? run.run_id : REDACTED_VALUE,
|
|
3108
3142
|
status: projectFreeformText(run.status || "unknown"),
|
|
3109
3143
|
mode: projectOptionalFreeformText(run.mode),
|
|
3110
3144
|
gate: projectOptionalFreeformText(pendingGate(run)),
|
|
@@ -3126,7 +3160,7 @@ function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
|
3126
3160
|
}
|
|
3127
3161
|
function fallbackRun(fallbackID, file, diagnostics) {
|
|
3128
3162
|
return {
|
|
3129
|
-
run_id:
|
|
3163
|
+
run_id: isDisplaySafeRunId(fallbackID) ? fallbackID : REDACTED_VALUE,
|
|
3130
3164
|
status: "invalid",
|
|
3131
3165
|
mode: null,
|
|
3132
3166
|
gate: null,
|
|
@@ -3424,7 +3458,7 @@ function renderRunTextFields(run) {
|
|
|
3424
3458
|
const slices = renderSliceLine(run?.slices);
|
|
3425
3459
|
const diagnostic = renderDiagnosticLine(run);
|
|
3426
3460
|
return {
|
|
3427
|
-
run_id:
|
|
3461
|
+
run_id: renderRunId(run?.run_id),
|
|
3428
3462
|
status_line: mode ? `${status} | ${mode}` : status,
|
|
3429
3463
|
gate_line: run?.gate ? `gate: ${renderFreeformText(run.gate)}` : null,
|
|
3430
3464
|
current_line: run?.current ? `current: ${renderFreeformText(run.current, 34)}` : null,
|
|
@@ -3438,6 +3472,10 @@ function renderRunTextFields(run) {
|
|
|
3438
3472
|
branch_line: run?.branch ? `branch: ${renderFreeformText(run.branch, 30)}` : null
|
|
3439
3473
|
};
|
|
3440
3474
|
}
|
|
3475
|
+
function renderRunId(value) {
|
|
3476
|
+
const segment = isDisplaySafeRunId(value) ? identitySegment(value) : freeformSegment(REDACTED_VALUE);
|
|
3477
|
+
return truncate(renderTerminalSegmentsOrFallback([segment]), 31);
|
|
3478
|
+
}
|
|
3441
3479
|
function renderHiddenRunsLine(value) {
|
|
3442
3480
|
const count = Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
3443
3481
|
return `+ ${renderTerminalSegmentsOrFallback([identitySegment(count)])} more runs`;
|
package/package.json
CHANGED
|
@@ -44,7 +44,10 @@ export function discoverCleanupSweepCandidates(repo, options = {}) {
|
|
|
44
44
|
try {
|
|
45
45
|
const result = classifyEntry(repositoryRoot, entry, collisions.get(entry.entryName) ?? false, {
|
|
46
46
|
...options,
|
|
47
|
-
registerTemporaryRef: (temporaryRef) =>
|
|
47
|
+
registerTemporaryRef: (temporaryRef, expectedOid) => {
|
|
48
|
+
temporaryRefs.push(temporaryRef);
|
|
49
|
+
options.registerTemporaryRef?.(temporaryRef, expectedOid);
|
|
50
|
+
},
|
|
48
51
|
});
|
|
49
52
|
return result.candidate;
|
|
50
53
|
} catch {
|
|
@@ -136,6 +139,7 @@ function worktreeClaimKeys(logical, options) {
|
|
|
136
139
|
|
|
137
140
|
function classifyEntry(repo, entry, sharedClaim, options) {
|
|
138
141
|
const evidence = createEmptyEvidence(entry.entryName, entry.logicalPath);
|
|
142
|
+
let fetchedBaseOid = null;
|
|
139
143
|
evidence.entry = {
|
|
140
144
|
kind: entry.kind,
|
|
141
145
|
logical_path: entry.logicalPath,
|
|
@@ -149,6 +153,7 @@ function classifyEntry(repo, entry, sharedClaim, options) {
|
|
|
149
153
|
const finish = (classification, reasonCodes, runId = evidence.run.run_id, temporaryRef = null) => ({
|
|
150
154
|
candidate: createCandidate({ entry_name: entry.entryName, run_id: runId, classification, reason_codes: sharedClaim ? [...reasonCodes, "SHARED_TARGET_CLAIM"] : reasonCodes, evidence }),
|
|
151
155
|
temporary_ref: temporaryRef,
|
|
156
|
+
base_oid: fetchedBaseOid,
|
|
152
157
|
});
|
|
153
158
|
|
|
154
159
|
if (entry.kind !== "directory" || entry.physicalPath !== entry.logicalPath || !SAFE_ENTRY.test(entry.entryName)) return finish("skipped", ["SKIPPED_UNSAFE_ENTRY"], null);
|
|
@@ -187,9 +192,9 @@ function classifyEntry(repo, entry, sharedClaim, options) {
|
|
|
187
192
|
if (pr.state === "OPEN") return finish("skipped", ["SKIPPED_PR_OPEN"]);
|
|
188
193
|
|
|
189
194
|
const temporaryRef = temporaryRefFor(entry.entryName, options.invocationId ?? randomUUID());
|
|
190
|
-
options.registerTemporaryRef
|
|
191
|
-
if (!
|
|
192
|
-
const targetResult = inspectTargets(repo, run, temporaryRef,
|
|
195
|
+
fetchedBaseOid = fetchVerifiedBase(repo, pr, temporaryRef, options.gitRunner ?? git, options.registerTemporaryRef);
|
|
196
|
+
if (!fetchedBaseOid) return finish("skipped", ["SKIPPED_BASE_UNPROVABLE"], run.run_id, temporaryRef);
|
|
197
|
+
const targetResult = inspectTargets(repo, run, temporaryRef, fetchedBaseOid, evidence, options.gitRunner ?? git, options);
|
|
193
198
|
if (targetResult.reason) return finish("skipped", [targetResult.reason], run.run_id, temporaryRef);
|
|
194
199
|
return finish("eligible", ["ELIGIBLE"], run.run_id, temporaryRef);
|
|
195
200
|
}
|
|
@@ -479,13 +484,27 @@ function inspectWorktree(repo, target, registered, branches, worktreeRoot, optio
|
|
|
479
484
|
return { record, reason: null };
|
|
480
485
|
}
|
|
481
486
|
|
|
482
|
-
function
|
|
483
|
-
if (!validBranch(repo, pr.base_ref, gitRunner)
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
+
function fetchVerifiedBase(repo, pr, temporaryRef, gitRunner, registerTemporaryRef) {
|
|
488
|
+
if (!validBranch(repo, pr.base_ref, gitRunner)) return null;
|
|
489
|
+
const remoteUrl = `https://github.com/${pr.repository}.git`;
|
|
490
|
+
const remoteRef = `refs/heads/${pr.base_ref}`;
|
|
491
|
+
const advertised = gitRunner(repo, ["ls-remote", "--exit-code", "--refs", remoteUrl, remoteRef]);
|
|
492
|
+
const baseOid = advertised?.ok ? exactRemoteOid(advertised.stdout, remoteRef) : null;
|
|
493
|
+
if (!baseOid) return null;
|
|
494
|
+
registerTemporaryRef?.(temporaryRef, baseOid);
|
|
495
|
+
if (gitRunner(repo, ["check-ref-format", temporaryRef])?.ok !== true) return null;
|
|
496
|
+
const fetch = gitRunner(repo, ["fetch", "--no-tags", "--no-recurse-submodules", "--no-write-fetch-head", "--force", remoteUrl, `+${baseOid}:${temporaryRef}`]);
|
|
497
|
+
if (!fetch?.ok) return null;
|
|
487
498
|
const resolved = gitRunner(repo, ["rev-parse", "--verify", `${temporaryRef}^{commit}`]);
|
|
488
|
-
return
|
|
499
|
+
return resolved?.ok && resolved.stdout.trim() === baseOid ? baseOid : null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function exactRemoteOid(stdout, expectedRef) {
|
|
503
|
+
if (typeof stdout !== "string") return null;
|
|
504
|
+
const lines = stdout.trimEnd().split("\n");
|
|
505
|
+
if (lines.length !== 1) return null;
|
|
506
|
+
const match = /^([0-9a-f]{40}|[0-9a-f]{64})\t([^\t\r\n]+)$/u.exec(lines[0]);
|
|
507
|
+
return match?.[2] === expectedRef && oid(match[1]) ? match[1] : null;
|
|
489
508
|
}
|
|
490
509
|
|
|
491
510
|
function validBranch(repo, branch, gitRunner) {
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
freeformSegment,
|
|
8
8
|
identitySegment,
|
|
9
|
+
isDisplaySafeRunId,
|
|
9
10
|
projectFreeformData,
|
|
10
11
|
renderTerminalSegments,
|
|
11
12
|
TRUSTED_SEGMENTS,
|
|
12
13
|
} from "./hardening/output-policy.js";
|
|
14
|
+
import { REDACTED_VALUE } from "./hardening/sensitive-data.js";
|
|
13
15
|
import { serializeTerminalJson } from "./hardening/terminal-encoding.js";
|
|
14
16
|
|
|
15
17
|
const CONFIRMATION_PREFIX = Object.freeze(["feature-factory", "factory", "cleanup", "--all", "--digest"]);
|
|
@@ -100,9 +102,12 @@ function exactReportConfirmation(report) {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function candidateLine(candidate) {
|
|
105
|
+
const entrySegment = candidate.entry_name === candidate.run_id && isDisplaySafeRunId(candidate.run_id)
|
|
106
|
+
? identitySegment(candidate.run_id)
|
|
107
|
+
: freeformSegment(REDACTED_VALUE);
|
|
103
108
|
return renderTerminalSegments([
|
|
104
109
|
identitySegment(candidate.classification), TRUSTED_SEGMENTS.TAB,
|
|
105
|
-
|
|
110
|
+
entrySegment, TRUSTED_SEGMENTS.TAB,
|
|
106
111
|
identitySegment(candidate.reason_codes.join(",")), TRUSTED_SEGMENTS.TAB,
|
|
107
112
|
freeformSegment(candidate.reasons.map((reason) => reason.message).join(" | ")),
|
|
108
113
|
]);
|
package/src/cleanup-sweep.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { cleanupRunLocked, CleanupRunChangedError, CleanupRunUnexpectedError } from "./factory.js";
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from "./cleanup-sweep-report.js";
|
|
20
20
|
import { RunJsonLockContendedError, withRunJsonLock } from "./run-state.js";
|
|
21
21
|
import { acquireLaunchFence, releaseLaunchFence } from "./process-evidence.js";
|
|
22
|
-
import { canonicalizeGithubPrUrl } from "./refs.js";
|
|
23
22
|
import { validateRun } from "./validate.js";
|
|
24
23
|
|
|
25
24
|
const utf8 = (left, right) => Buffer.from(left, "utf8").compare(Buffer.from(right, "utf8"));
|
|
@@ -37,7 +36,6 @@ export async function previewCleanupSweep(options = {}) {
|
|
|
37
36
|
const discovery = discoverCleanupSweepCandidates(repository.root_path, tracked.options);
|
|
38
37
|
discoveryComplete = true;
|
|
39
38
|
candidates = discovery.candidates;
|
|
40
|
-
temporaryRefs.push(...temporaryRefRecords(discovery.temporary_refs, candidates, repository, tracked.prBaseOids));
|
|
41
39
|
const digest = createCleanupSweepDigest(repository, candidates);
|
|
42
40
|
report = createCleanupSweepReport({
|
|
43
41
|
mode: "preview",
|
|
@@ -74,7 +72,6 @@ export async function executeCleanupSweep(options = {}) {
|
|
|
74
72
|
const discovery = discoverCleanupSweepCandidates(repository.root_path, tracked.options);
|
|
75
73
|
discoveryComplete = true;
|
|
76
74
|
candidates = discovery.candidates;
|
|
77
|
-
temporaryRefs.push(...temporaryRefRecords(discovery.temporary_refs, candidates, repository, tracked.prBaseOids));
|
|
78
75
|
const comparison = compareCleanupSweepDigest(options.digest, repository, candidates);
|
|
79
76
|
await phase(options, "after-digest-recompute", { repository, candidates });
|
|
80
77
|
if (!comparison.matched) {
|
|
@@ -187,7 +184,7 @@ async function executeCandidate(repository, authorized, options, invocationId, t
|
|
|
187
184
|
if (authorizationRecord(normalized) !== authorizationRecord(authorized)) return changedCandidate(normalized);
|
|
188
185
|
|
|
189
186
|
const baseRef = reclassified.temporary_ref;
|
|
190
|
-
const baseOid =
|
|
187
|
+
const baseOid = reclassified.base_oid;
|
|
191
188
|
const baseState = inspectAuthorizedTemporaryRef(repository.root_path, baseRef, baseOid, mutationGitRunner);
|
|
192
189
|
if (baseState === "changed") return changedCandidate(normalized);
|
|
193
190
|
if (baseState !== "matching") return inspectionFailureCandidate(normalized);
|
|
@@ -214,7 +211,7 @@ async function executeCandidate(repository, authorized, options, invocationId, t
|
|
|
214
211
|
fetchedBaseRef: baseOid,
|
|
215
212
|
fetchedBase: { ref: baseRef, oid: baseOid },
|
|
216
213
|
gitRunner: guardedCleanupGitRunner(repository.root_path, baseRef, baseOid, mutationGitRunner),
|
|
217
|
-
assertMutationAuthorized: () => assertCandidateMutationAuthorized(repository, authorized, guardedOptions, invocationId, temporaryRefs),
|
|
214
|
+
assertMutationAuthorized: () => assertCandidateMutationAuthorized(repository, authorized, baseOid, guardedOptions, invocationId, temporaryRefs),
|
|
218
215
|
removeRunDir: options.removeRunDir,
|
|
219
216
|
checkWorktreeIdentity: options.checkWorktreeIdentity,
|
|
220
217
|
physicalPath: options.physicalPath,
|
|
@@ -337,67 +334,21 @@ function discoveryOptions(options, invocationId) {
|
|
|
337
334
|
processOptions: { ...options, ...options.processOptions },
|
|
338
335
|
clock: options.clock,
|
|
339
336
|
invocationId,
|
|
337
|
+
registerTemporaryRef: options.registerTemporaryRef,
|
|
340
338
|
};
|
|
341
339
|
}
|
|
342
340
|
|
|
343
|
-
function temporaryRefRecords(refs, candidates, repository, prBaseOids) {
|
|
344
|
-
return refs.map((ref) => {
|
|
345
|
-
const candidate = candidates.find((item) => ref.endsWith(sha256RunId(item.entry_name)));
|
|
346
|
-
const expectedOid = candidate?.evidence?.pr?.base_sha ?? recordedRunBaseOid(repository.root_path, candidate?.entry_name, prBaseOids);
|
|
347
|
-
return { ref, expected_oid: expectedOid ?? null };
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
341
|
function trackedDiscoveryOptions(options, invocationId, temporaryRefs) {
|
|
352
|
-
const githubRunner = options.githubRunner ?? github;
|
|
353
|
-
const gitRunner = options.gitRunner ?? git;
|
|
354
|
-
const prBaseOids = new Map();
|
|
355
|
-
let currentBaseOid = null;
|
|
356
|
-
const trackedGithubRunner = (...args) => {
|
|
357
|
-
currentBaseOid = null;
|
|
358
|
-
const result = githubRunner(...args);
|
|
359
|
-
try {
|
|
360
|
-
const body = result?.ok ? JSON.parse(result.stdout) : null;
|
|
361
|
-
if (typeof body?.html_url === "string" && typeof body?.base?.sha === "string") {
|
|
362
|
-
currentBaseOid = body.base.sha;
|
|
363
|
-
prBaseOids.set(canonicalizeGithubPrUrl(body.html_url), currentBaseOid);
|
|
364
|
-
}
|
|
365
|
-
} catch {
|
|
366
|
-
// Invalid responses cannot authorize temporary-ref deletion.
|
|
367
|
-
}
|
|
368
|
-
return result;
|
|
369
|
-
};
|
|
370
|
-
const trackedGitRunner = (cwd, args, commandOptions) => {
|
|
371
|
-
if (args[0] === "check-ref-format" && typeof args[1] === "string" && args[1].startsWith("refs/feature-factory/cleanup-sweep/")) {
|
|
372
|
-
temporaryRefs.push({ ref: args[1], expected_oid: currentBaseOid });
|
|
373
|
-
}
|
|
374
|
-
return gitRunner(cwd, args, commandOptions);
|
|
375
|
-
};
|
|
376
342
|
return {
|
|
377
|
-
options: discoveryOptions({
|
|
378
|
-
|
|
343
|
+
options: discoveryOptions({
|
|
344
|
+
...options,
|
|
345
|
+
githubRunner: options.githubRunner ?? github,
|
|
346
|
+
gitRunner: options.gitRunner ?? git,
|
|
347
|
+
registerTemporaryRef: (ref, expectedOid) => temporaryRefs.push({ ref, expected_oid: expectedOid }),
|
|
348
|
+
}, invocationId),
|
|
379
349
|
};
|
|
380
350
|
}
|
|
381
351
|
|
|
382
|
-
function recordedRunBaseOid(repo, entryName, prBaseOids) {
|
|
383
|
-
if (!entryName) return null;
|
|
384
|
-
try {
|
|
385
|
-
const run = JSON.parse(readFileSync(join(repo, ".opencode", "factory", entryName, "run.json"), "utf8"));
|
|
386
|
-
const urls = [run?.pr_url, run?.terminal_result?.pr_url].filter((value) => typeof value === "string");
|
|
387
|
-
for (const url of urls) {
|
|
388
|
-
const canonical = canonicalizeGithubPrUrl(url);
|
|
389
|
-
if (prBaseOids.has(canonical)) return prBaseOids.get(canonical);
|
|
390
|
-
}
|
|
391
|
-
} catch {
|
|
392
|
-
// Missing authorization is handled as FAILED_TEMP_REF_CLEANUP if the ref exists.
|
|
393
|
-
}
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function sha256RunId(value) {
|
|
398
|
-
return createHash("sha256").update(value).digest("hex");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
352
|
async function finalizeAfterTemporaryRefs(report, context) {
|
|
402
353
|
try {
|
|
403
354
|
await cleanupTemporaryRefs(context.temporaryRefs, context);
|
|
@@ -453,7 +404,7 @@ async function cleanupTemporaryRefs(records, options) {
|
|
|
453
404
|
}
|
|
454
405
|
|
|
455
406
|
function reclassifyHeldCandidate(repository, authorized, options, invocationId, temporaryRefs) {
|
|
456
|
-
const registerTemporaryRef = (ref) => temporaryRefs.push({ ref, expected_oid:
|
|
407
|
+
const registerTemporaryRef = (ref, expectedOid) => temporaryRefs.push({ ref, expected_oid: expectedOid });
|
|
457
408
|
const result = classifyCleanupSweepCandidate(repository.root_path, authorized.entry_name, {
|
|
458
409
|
...discoveryOptions(options, invocationId),
|
|
459
410
|
heldRunId: authorized.run_id,
|
|
@@ -483,13 +434,14 @@ function guardedCleanupGitRunner(repo, baseRef, baseOid, gitRunner) {
|
|
|
483
434
|
};
|
|
484
435
|
}
|
|
485
436
|
|
|
486
|
-
function assertCandidateMutationAuthorized(repository, authorized, options, invocationId, temporaryRefs) {
|
|
437
|
+
function assertCandidateMutationAuthorized(repository, authorized, authorizedBaseOid, options, invocationId, temporaryRefs) {
|
|
487
438
|
const reclassified = reclassifyHeldCandidate(repository, authorized, options, invocationId, temporaryRefs);
|
|
488
439
|
const normalized = normalizeHeldLockEvidence(reclassified.candidate);
|
|
489
|
-
if (
|
|
440
|
+
if (reclassified.base_oid !== authorizedBaseOid
|
|
441
|
+
|| progressiveAuthorizationRecord(normalized) !== progressiveAuthorizationRecord(authorized)) throw new CleanupRunChangedError();
|
|
490
442
|
}
|
|
491
443
|
|
|
492
|
-
function
|
|
444
|
+
function progressiveAuthorizationRecord(candidate) {
|
|
493
445
|
return canonicalJson({
|
|
494
446
|
entry_name: candidate.entry_name,
|
|
495
447
|
run_id: candidate.run_id,
|
|
@@ -499,6 +451,7 @@ function sidecarAuthorizationRecord(candidate) {
|
|
|
499
451
|
heartbeat: candidate.evidence.heartbeat,
|
|
500
452
|
process: candidate.evidence.process,
|
|
501
453
|
launch_claim: candidate.evidence.launch_claim,
|
|
454
|
+
pr: candidate.evidence.pr,
|
|
502
455
|
});
|
|
503
456
|
}
|
|
504
457
|
|
package/src/cli-output.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
freeformSegment,
|
|
3
3
|
identitySegment,
|
|
4
|
+
isDisplaySafeRunId,
|
|
4
5
|
projectFreeformData,
|
|
5
6
|
renderTerminalSegments,
|
|
6
7
|
TRUSTED_SEGMENTS,
|
|
@@ -14,7 +15,6 @@ const VALIDATED_STATUS_VALUES = new Set([
|
|
|
14
15
|
"ok", "partial", "pending", "ready", "rejected", "review", "running", "stopped",
|
|
15
16
|
"unavailable", "warn", "warning",
|
|
16
17
|
]);
|
|
17
|
-
const SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
18
18
|
const SAFE_UUID_PATTERN = /^[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}$/u;
|
|
19
19
|
const SAFE_HASH_PATTERN = /^(?:sha256:)?[A-Fa-f0-9]{32,128}$/u;
|
|
20
20
|
const SAFE_REF_CHARACTERS = /^[A-Za-z0-9._/-]+$/u;
|
|
@@ -141,7 +141,7 @@ function projectedValueSegment(key, value) {
|
|
|
141
141
|
function validatedIdentity(key, value) {
|
|
142
142
|
if (typeof value !== "string") return false;
|
|
143
143
|
if (key === "status" || key === "level") return VALIDATED_STATUS_VALUES.has(value);
|
|
144
|
-
if (key === "run_id") return
|
|
144
|
+
if (key === "run_id") return isDisplaySafeRunId(value);
|
|
145
145
|
if (key === "token" || key === "boundary_token" || key === "action_token" || key === "fence_token") {
|
|
146
146
|
// Factory-issued tokens have exactly one shape: the UUID minted at
|
|
147
147
|
// boundary/action/fence creation (run-state.js randomUUID()); operators only
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
REDACTED_VALUE,
|
|
3
3
|
SCRUB_MARKERS,
|
|
4
|
+
containsRecognizedSensitiveFragment,
|
|
4
5
|
isSensitiveKey,
|
|
6
|
+
isRecognizedSensitiveValue,
|
|
7
|
+
isSensitiveValue,
|
|
5
8
|
scrubSensitiveData,
|
|
6
9
|
scrubSensitiveString,
|
|
7
10
|
} from "./sensitive-data.js";
|
|
@@ -14,6 +17,9 @@ export const DIAGNOSTIC_FREEFORM_FIELDS = Object.freeze([
|
|
|
14
17
|
]);
|
|
15
18
|
|
|
16
19
|
const OUTPUT_POLICY_ERROR_CODE = "ERR_OUTPUT_POLICY";
|
|
20
|
+
const SAFE_RUN_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
21
|
+
const DESCRIPTIVE_RUN_ID_PATTERN = /^[a-z]{1,32}(?:-[a-z]{1,32})+(?:-[0-9]{1,6})?$/u;
|
|
22
|
+
const UUID_PATTERN = /[A-Fa-f0-9]{8}(?:-[A-Fa-f0-9]{4}){3}-[A-Fa-f0-9]{12}/u;
|
|
17
23
|
const segmentBrand = new WeakSet();
|
|
18
24
|
const structuredErrorBrand = new WeakSet();
|
|
19
25
|
const structuredErrorSegments = new WeakMap();
|
|
@@ -92,6 +98,14 @@ export function projectFreeformData(value) {
|
|
|
92
98
|
try { return scrubSensitiveData(value, { mode: "baseline" }); } catch { fail("projection"); }
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
export function isDisplaySafeRunId(value) {
|
|
102
|
+
if (typeof value !== "string" || !SAFE_RUN_ID_PATTERN.test(value) || value.includes("..") || value.endsWith(".lock") || UUID_PATTERN.test(value)) return false;
|
|
103
|
+
if (isRecognizedSensitiveValue(value, { mode: "baseline" }) || containsRecognizedSensitiveFragment(value)) return false;
|
|
104
|
+
if (!isSensitiveValue(value, { mode: "baseline" })) return true;
|
|
105
|
+
return DESCRIPTIVE_RUN_ID_PATTERN.test(value)
|
|
106
|
+
&& value.split("-").every((segment) => /^\d+$/u.test(segment) || !isSensitiveValue(segment, { mode: "baseline" }));
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
export function projectDiagnosticData(value, options = undefined) {
|
|
96
110
|
try {
|
|
97
111
|
const policy = readDiagnosticPolicy(options);
|
|
@@ -16,6 +16,19 @@ const TOKEN_SHAPED_VALUE_PATTERNS = Object.freeze([
|
|
|
16
16
|
/(?:^|[^A-Za-z0-9-])(?:[A-Fa-f0-9]{32,})(?=$|[^A-Za-z0-9-])/u,
|
|
17
17
|
/(?:https?|ssh|git|ftp):\/\/[^/\s:@]+:[^/\s@]+@/iu,
|
|
18
18
|
]);
|
|
19
|
+
const TOKEN_SHAPED_FRAGMENT_PATTERNS = Object.freeze([
|
|
20
|
+
/gh[pousr]_[A-Za-z0-9_]{20,}/iu,
|
|
21
|
+
/github_pat_[A-Za-z0-9_]{20,}/iu,
|
|
22
|
+
/hc_[A-Za-z0-9][A-Za-z0-9_-]{10,}/iu,
|
|
23
|
+
/sk-proj[-_][A-Za-z0-9_-]{20,}/iu,
|
|
24
|
+
/sk-[A-Za-z0-9_-]{20,}/iu,
|
|
25
|
+
/xox[abp][_-][A-Za-z0-9-]{10,}(?:-[A-Za-z0-9-]{10,})*/iu,
|
|
26
|
+
/glpat-[A-Za-z0-9_-]{20,}/iu,
|
|
27
|
+
/Bearer\s+[A-Za-z0-9._~+/=-]{20,}/iu,
|
|
28
|
+
/eyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/iu,
|
|
29
|
+
/(?:AKIA|ASIA)[A-Z0-9]{16}/u,
|
|
30
|
+
/[A-Fa-f0-9]{32,}/u,
|
|
31
|
+
]);
|
|
19
32
|
const ENDPOINT_PROVIDER_PREFIX_PATTERN = /(?:hc[a-z0-9_-]*|gh[pousr]|github_pat|sk(?:-proj)?|xox[abp]|glpat)[_-][A-Za-z0-9_-]{10,}/iu;
|
|
20
33
|
const ENDPOINT_BARE_HEX_PATTERN = /^[A-Fa-f0-9]{32,}$/u;
|
|
21
34
|
const ENDPOINT_SECRET_ASSIGNMENT_PATTERN = /(?:(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key)\s*[=:]|[=:]\s*(?:key|token|secret|password|authorization|credential|access|team|api[_-]?key))/iu;
|
|
@@ -53,6 +66,10 @@ export function isSecretShapedKey(value, { mode = "baseline" } = {}) {
|
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
export function isSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
69
|
+
return isRecognizedSensitiveValue(value, { mode }) || highEntropyValue(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isRecognizedSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
56
73
|
assertMode(mode);
|
|
57
74
|
if (typeof value !== "string") return false;
|
|
58
75
|
const trimmed = value.trim();
|
|
@@ -61,10 +78,20 @@ export function isSensitiveValue(value, { mode = "baseline" } = {}) {
|
|
|
61
78
|
|| BASELINE_SENSITIVE_VALUE_PATTERN.test(trimmed)
|
|
62
79
|
|| tokenShapedValue(trimmed)
|
|
63
80
|
|| credentialBearingUrl(trimmed)
|
|
64
|
-
|| highEntropySingleToken(trimmed)
|
|
65
81
|
|| (mode === "endpoint" && endpointValueLooksSensitive(trimmed));
|
|
66
82
|
}
|
|
67
83
|
|
|
84
|
+
export function containsRecognizedSensitiveFragment(value) {
|
|
85
|
+
if (typeof value !== "string") return false;
|
|
86
|
+
return TOKEN_SHAPED_FRAGMENT_PATTERNS.some((pattern) => pattern.test(value));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function highEntropyValue(value) {
|
|
90
|
+
if (typeof value !== "string") return false;
|
|
91
|
+
const trimmed = value.trim();
|
|
92
|
+
return Boolean(trimmed && highEntropySingleToken(trimmed));
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
export function scrubSensitiveString(value, { mode = "baseline" } = {}) {
|
|
69
96
|
assertMode(mode);
|
|
70
97
|
if (typeof value !== "string") return SCRUB_MARKERS.unsupported;
|
package/src/tui-data.js
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
} from "./factory-diagnostics.js";
|
|
14
14
|
import {
|
|
15
15
|
freeformSegment,
|
|
16
|
+
isDisplaySafeRunId,
|
|
16
17
|
projectDiagnosticData,
|
|
17
18
|
projectFreeformData,
|
|
18
19
|
renderTerminalSegmentsOrFallback,
|
|
19
20
|
} from "./hardening/output-policy.js";
|
|
21
|
+
import { REDACTED_VALUE } from "./hardening/sensitive-data.js";
|
|
20
22
|
|
|
21
23
|
const SKIP_DIRS = new Set([".git", "node_modules", "dist", "coverage", ".cache", ".next"]);
|
|
22
24
|
const MAX_SCAN_DIRS = 2000;
|
|
@@ -177,7 +179,7 @@ function parseErrorDiagnostics(file, error) {
|
|
|
177
179
|
|
|
178
180
|
function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
179
181
|
return {
|
|
180
|
-
run_id:
|
|
182
|
+
run_id: run.run_id === fallbackID && isDisplaySafeRunId(run.run_id) ? run.run_id : REDACTED_VALUE,
|
|
181
183
|
status: projectFreeformText(run.status || "unknown"),
|
|
182
184
|
mode: projectOptionalFreeformText(run.mode),
|
|
183
185
|
gate: projectOptionalFreeformText(pendingGate(run)),
|
|
@@ -200,7 +202,7 @@ function summarize(run, fallbackID, file, diagnostics = healthyDiagnostics()) {
|
|
|
200
202
|
|
|
201
203
|
function fallbackRun(fallbackID, file, diagnostics) {
|
|
202
204
|
return {
|
|
203
|
-
run_id:
|
|
205
|
+
run_id: isDisplaySafeRunId(fallbackID) ? fallbackID : REDACTED_VALUE,
|
|
204
206
|
status: "invalid",
|
|
205
207
|
mode: null,
|
|
206
208
|
gate: null,
|
package/src/tui-rendering.js
CHANGED
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
freeformSegment,
|
|
7
7
|
identitySegment,
|
|
8
|
+
isDisplaySafeRunId,
|
|
8
9
|
renderTerminalSegmentsOrFallback,
|
|
9
10
|
TRUSTED_SEGMENTS,
|
|
10
11
|
} from "./hardening/output-policy.js";
|
|
12
|
+
import { REDACTED_VALUE } from "./hardening/sensitive-data.js";
|
|
11
13
|
|
|
12
14
|
const RUN_STATUSES = new Set(["running", "completed", "blocked", "partial", "needs-human", "invalid"]);
|
|
13
15
|
const RUN_MODES = new Set(["interactive", "headless", "autonomous"]);
|
|
@@ -49,7 +51,7 @@ export function renderRunTextFields(run) {
|
|
|
49
51
|
const slices = renderSliceLine(run?.slices);
|
|
50
52
|
const diagnostic = renderDiagnosticLine(run);
|
|
51
53
|
return {
|
|
52
|
-
run_id:
|
|
54
|
+
run_id: renderRunId(run?.run_id),
|
|
53
55
|
status_line: mode ? `${status} | ${mode}` : status,
|
|
54
56
|
gate_line: run?.gate ? `gate: ${renderFreeformText(run.gate)}` : null,
|
|
55
57
|
current_line: run?.current ? `current: ${renderFreeformText(run.current, 34)}` : null,
|
|
@@ -64,6 +66,11 @@ export function renderRunTextFields(run) {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function renderRunId(value) {
|
|
70
|
+
const segment = isDisplaySafeRunId(value) ? identitySegment(value) : freeformSegment(REDACTED_VALUE);
|
|
71
|
+
return truncate(renderTerminalSegmentsOrFallback([segment]), 31);
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
export function renderHiddenRunsLine(value) {
|
|
68
75
|
const count = Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
69
76
|
return `+ ${renderTerminalSegmentsOrFallback([identitySegment(count)])} more runs`;
|