@sun-asterisk/sungen 3.3.0-alpha.7 → 3.3.0-alpha.8
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/dist/cli/case-triage-hint.d.ts +14 -0
- package/dist/cli/case-triage-hint.d.ts.map +1 -0
- package/dist/cli/case-triage-hint.js +23 -0
- package/dist/cli/case-triage-hint.js.map +1 -0
- package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
- package/dist/cli/commands/contract-derive-command.js +2 -1
- package/dist/cli/commands/contract-derive-command.js.map +1 -1
- package/dist/cli/commands/contract.d.ts.map +1 -1
- package/dist/cli/commands/contract.js +5 -2
- package/dist/cli/commands/contract.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +29 -13
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/gate.d.ts.map +1 -1
- package/dist/cli/commands/gate.js +2 -1
- package/dist/cli/commands/gate.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +12 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +4 -2
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/next.d.ts +4 -2
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +22 -6
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +5 -3
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/project-sungen-invocation.d.ts +6 -0
- package/dist/cli/project-sungen-invocation.d.ts.map +1 -1
- package/dist/cli/project-sungen-invocation.js +19 -0
- package/dist/cli/project-sungen-invocation.js.map +1 -1
- package/dist/cli/run-command-hints.d.ts +9 -0
- package/dist/cli/run-command-hints.d.ts.map +1 -0
- package/dist/cli/run-command-hints.js +84 -0
- package/dist/cli/run-command-hints.js.map +1 -0
- package/dist/cli/waiting-for-you-block.d.ts +9 -6
- package/dist/cli/waiting-for-you-block.d.ts.map +1 -1
- package/dist/cli/waiting-for-you-block.js +9 -6
- package/dist/cli/waiting-for-you-block.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +11 -2
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +8 -4
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +6 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +11 -4
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +3 -2
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +4 -4
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/graph/evaluate-graph.d.ts +3 -1
- package/dist/graph/evaluate-graph.d.ts.map +1 -1
- package/dist/graph/evaluate-graph.js +4 -2
- package/dist/graph/evaluate-graph.js.map +1 -1
- package/dist/graph/unit-artifact-probes.d.ts.map +1 -1
- package/dist/graph/unit-artifact-probes.js +3 -1
- package/dist/graph/unit-artifact-probes.js.map +1 -1
- package/dist/harness/gates/audit-report-verification.d.ts +2 -0
- package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
- package/dist/harness/gates/audit-report-verification.js +8 -3
- package/dist/harness/gates/audit-report-verification.js.map +1 -1
- package/dist/harness/next-step.d.ts +2 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +2 -0
- package/dist/harness/next-step.js.map +1 -1
- package/dist/hooks/hand-back-core.d.ts +4 -1
- package/dist/hooks/hand-back-core.d.ts.map +1 -1
- package/dist/hooks/hand-back-core.js +7 -3
- package/dist/hooks/hand-back-core.js.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.js +1 -1
- package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/case-triage-hint.ts +31 -0
- package/src/cli/commands/contract-derive-command.ts +2 -1
- package/src/cli/commands/contract.ts +5 -2
- package/src/cli/commands/delivery.ts +29 -13
- package/src/cli/commands/gate.ts +2 -1
- package/src/cli/commands/generate.ts +11 -2
- package/src/cli/commands/journey.ts +4 -2
- package/src/cli/commands/next.ts +25 -9
- package/src/cli/commands/triage.ts +5 -3
- package/src/cli/project-sungen-invocation.ts +20 -0
- package/src/cli/run-command-hints.ts +47 -0
- package/src/cli/waiting-for-you-block.ts +14 -10
- package/src/exporters/csv-exporter.ts +12 -4
- package/src/exporters/matrix/export.ts +11 -4
- package/src/exporters/xlsx-exporter.ts +5 -4
- package/src/graph/evaluate-graph.ts +5 -3
- package/src/graph/unit-artifact-probes.ts +3 -1
- package/src/harness/gates/audit-report-verification.ts +7 -2
- package/src/harness/next-step.ts +3 -0
- package/src/hooks/hand-back-core.ts +8 -4
- package/src/hooks/session-start-unit-state-hook.ts +1 -1
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The "Waiting for you" block (#954) — every pending human-only action of a unit, as ready-to-copy
|
|
3
3
|
* commands with the reason under each. One renderer for the three places that show it: `sungen next`
|
|
4
|
-
* (text) and the Stop / SessionStart hook context. Commands
|
|
5
|
-
* with a local install, #939); `--json` keeps
|
|
6
|
-
*
|
|
4
|
+
* (text) and the Stop / SessionStart hook context. Commands — and those inside a reason (#963) — as the
|
|
5
|
+
* project runs sungen (`npx sungen` with a local install, #939); `--json` keeps `command` canonical in
|
|
6
|
+
* `humanActions` and adds `rendered`. An instruction (a commit through review, a `git checkout --`) is
|
|
7
|
+
* marked `✎ … (by hand, not a command)`, never shown as one to copy.
|
|
7
8
|
*/
|
|
8
9
|
import type { HumanAction } from '../graph/human-action';
|
|
9
|
-
import { withProjectInvocation } from './project-sungen-invocation';
|
|
10
|
+
import { withProjectInvocation, withProjectInvocationInText } from './project-sungen-invocation';
|
|
10
11
|
|
|
11
12
|
export const WAITING_FOR_YOU_HEAD = '⏳ Waiting for you — a human runs these in their own terminal (in Claude Code: ! <command>); an agent never runs them:';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* The block's lines (indented two spaces), or none when nothing waits for a human
|
|
15
|
-
* of the blocking step that cut the frontier — the block is not exhaustive then,
|
|
15
|
+
* The block's lines (indented two spaces), or none when nothing waits for a human and nothing holds the
|
|
16
|
+
* rest. `heldBy`: the title of the blocking step that cut the frontier — the block is not exhaustive then,
|
|
17
|
+
* and says what holds the rest, with that step's command (`heldByCommand`) when given (#966). A held
|
|
18
|
+
* block with no item of its own still says so, rather than vanishing (a QA read "nothing is waiting").
|
|
16
19
|
*/
|
|
17
|
-
export function waitingForYouLines(actions: readonly HumanAction[] | undefined, projectRoot: string, heldBy?: string): string[] {
|
|
18
|
-
|
|
20
|
+
export function waitingForYouLines(actions: readonly HumanAction[] | undefined, projectRoot: string, heldBy?: string, heldByCommand?: string): string[] {
|
|
21
|
+
const holds = heldBy ? `"${heldBy}" clears${heldByCommand ? ` (the step above: ${withProjectInvocationInText(heldByCommand, projectRoot)})` : ''}` : '';
|
|
22
|
+
if (!actions?.length) return heldBy ? [` ⏳ Waiting for you: nothing yet — more appear once ${holds}.`] : [];
|
|
19
23
|
return [` ${WAITING_FOR_YOU_HEAD}`, ...actions.flatMap((a) => [
|
|
20
24
|
a.instruction ? ` ✎ ${a.command} (by hand, not a command)` : ` ${withProjectInvocation(a.command, projectRoot)}`,
|
|
21
|
-
` ${a.reason}`,
|
|
22
|
-
]), ...(heldBy ? [` More appear once
|
|
25
|
+
` ${withProjectInvocationInText(a.reason, projectRoot)}`,
|
|
26
|
+
]), ...(heldBy ? [` More appear once ${holds}.`] : [])];
|
|
23
27
|
}
|
|
@@ -439,12 +439,12 @@ export function renderCsv(summary: ScreenSummary, rows: TestCaseRow[], specLink:
|
|
|
439
439
|
* multi-locale delivery aggregator. Pass an empty string to force the
|
|
440
440
|
* base (no suffix) variant.
|
|
441
441
|
*/
|
|
442
|
-
export function writeCsv(cwd: string, screen: string, csvContent: string, locale?: string | null): string {
|
|
442
|
+
export function writeCsv(cwd: string, screen: string, csvContent: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
443
443
|
const outDir = path.join(cwd, 'qa', 'deliverables');
|
|
444
444
|
if (!fs.existsSync(outDir)) {
|
|
445
445
|
fs.mkdirSync(outDir, { recursive: true });
|
|
446
446
|
}
|
|
447
|
-
const outPath = path.join(outDir, `${deliverableBasename(screen, locale)}.csv`);
|
|
447
|
+
const outPath = path.join(outDir, `${deliverableBasename(screen, locale, opts)}.csv`);
|
|
448
448
|
fs.writeFileSync(outPath, csvContent, 'utf-8');
|
|
449
449
|
return outPath;
|
|
450
450
|
}
|
|
@@ -455,8 +455,16 @@ export function writeCsv(cwd: string, screen: string, csvContent: string, locale
|
|
|
455
455
|
* locale is given (or `SUNGEN_ENV` is set and no explicit override). Pass
|
|
456
456
|
* `null` / empty string to force the base variant.
|
|
457
457
|
*/
|
|
458
|
-
export function deliverableBasename(screen: string, locale?: string | null): string {
|
|
458
|
+
export function deliverableBasename(screen: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
459
459
|
const effective = locale !== undefined ? locale : process.env.SUNGEN_ENV;
|
|
460
|
-
return effective ? `${screen}-testcases.${effective}` : `${screen}-testcases`;
|
|
460
|
+
return `${effective ? `${screen}-testcases.${effective}` : `${screen}-testcases`}${opts.draft ? DRAFT_MARK : ''}`;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
+
/**
|
|
464
|
+
* A DRAFT render (a watermarked `--preview`) is written beside the official deliverable, never onto it
|
|
465
|
+
* (#965): `<screen>-testcases[.env].DRAFT.<ext>`. Readers of `qa/deliverables/` skip it (isDraftDeliverable).
|
|
466
|
+
*/
|
|
467
|
+
export interface DeliverableFileOptions { draft?: boolean }
|
|
468
|
+
export const DRAFT_MARK = '.DRAFT';
|
|
469
|
+
export const isDraftDeliverable = (fileName: string): boolean => /\.DRAFT\.[^.]+$/.test(fileName);
|
|
470
|
+
|
|
@@ -169,7 +169,12 @@ export function approveMatrix(paths: MatrixTargetPaths, groupIds?: string[]): {
|
|
|
169
169
|
|
|
170
170
|
export type MatrixFormat = 'xlsx' | 'csv' | 'both';
|
|
171
171
|
|
|
172
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Render + write the requested format(s). Caller has already enforced the gate policy. A draft (the
|
|
174
|
+
* matrix is not approved for official delivery: it carries the DRAFT banner) goes to its own
|
|
175
|
+
* `…-testcases.DRAFT.<ext>` and never overwrites the official file (#965); the tester's Manual results
|
|
176
|
+
* are still carried from the official workbook when it can be read.
|
|
177
|
+
*/
|
|
173
178
|
export async function writeMatrixDeliverables(
|
|
174
179
|
paths: MatrixTargetPaths,
|
|
175
180
|
model: MatrixModel,
|
|
@@ -179,23 +184,25 @@ export async function writeMatrixDeliverables(
|
|
|
179
184
|
): Promise<{ csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport }> {
|
|
180
185
|
const out: { csvPath?: string; xlsxPath?: string; manualCarry?: ManualCarryReport } = {};
|
|
181
186
|
const xlsx = format === 'xlsx' || format === 'both';
|
|
187
|
+
const draft = model.manifest.approvalState === 'draft';
|
|
182
188
|
// Read the workbook about to be overwritten BEFORE writing anything, so the tester's Manual
|
|
183
189
|
// results survive (#756) and a refusal leaves no half-written target behind.
|
|
184
190
|
// A CSV-only export overwrites no workbook, so an unreadable one is no reason to refuse it — but a
|
|
185
191
|
// readable one still holds the Manual results the CSV should show.
|
|
192
|
+
// A draft overwrites nothing official either, so an unreadable official workbook is no reason to refuse it.
|
|
186
193
|
const prior = priorRead !== undefined ? priorRead
|
|
187
|
-
: xlsx ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
|
|
194
|
+
: xlsx && !draft ? await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit))
|
|
188
195
|
: await readPriorManualResults(deliverableXlsxPath(paths.cwd, paths.unit)).catch(() => null);
|
|
189
196
|
// Into the model first: the CSV, every cached total and roll-up, and the caller's summary
|
|
190
197
|
// all read it (#891).
|
|
191
198
|
applyManualCarry(model, prior);
|
|
192
199
|
if (format === 'csv' || format === 'both') {
|
|
193
|
-
out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model));
|
|
200
|
+
out.csvPath = writeCsv(paths.cwd, paths.unit, renderMatrixCsv(model), undefined, { draft });
|
|
194
201
|
}
|
|
195
202
|
if (xlsx) {
|
|
196
203
|
const wb = renderMatrixXlsx(model, getPackageVersion());
|
|
197
204
|
out.manualCarry = carryManualResults(wb, model, prior);
|
|
198
|
-
out.xlsxPath = await writeXlsx(paths.cwd, paths.unit, wb);
|
|
205
|
+
out.xlsxPath = await writeXlsx(paths.cwd, paths.unit, wb, undefined, { draft });
|
|
199
206
|
}
|
|
200
207
|
return out;
|
|
201
208
|
}
|
|
@@ -18,7 +18,7 @@ import ExcelJS from 'exceljs';
|
|
|
18
18
|
import JSZip from 'jszip';
|
|
19
19
|
import { ApiCatalogEntry, ApiDetailRow, ScreenSummary, TestCaseRow } from './types';
|
|
20
20
|
import { getPackageVersion } from './package-info';
|
|
21
|
-
import { deliverableBasename } from './csv-exporter';
|
|
21
|
+
import { deliverableBasename, DeliverableFileOptions } from './csv-exporter';
|
|
22
22
|
import {
|
|
23
23
|
extractApiCallOrder,
|
|
24
24
|
extractCasesDataset,
|
|
@@ -114,8 +114,8 @@ export function buildApiDetailRows(
|
|
|
114
114
|
* to force the base (no suffix) variant.
|
|
115
115
|
*/
|
|
116
116
|
/** Where `writeXlsx` puts a unit's workbook — shared so a reader targets the identical file. */
|
|
117
|
-
export function deliverableXlsxPath(cwd: string, screen: string, locale?: string | null): string {
|
|
118
|
-
return path.join(cwd, 'qa', 'deliverables', `${deliverableBasename(screen, locale)}.xlsx`);
|
|
117
|
+
export function deliverableXlsxPath(cwd: string, screen: string, locale?: string | null, opts: DeliverableFileOptions = {}): string {
|
|
118
|
+
return path.join(cwd, 'qa', 'deliverables', `${deliverableBasename(screen, locale, opts)}.xlsx`);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export async function writeXlsx(
|
|
@@ -123,8 +123,9 @@ export async function writeXlsx(
|
|
|
123
123
|
screen: string,
|
|
124
124
|
wb: ExcelJS.Workbook,
|
|
125
125
|
locale?: string | null,
|
|
126
|
+
opts: DeliverableFileOptions = {},
|
|
126
127
|
): Promise<string> {
|
|
127
|
-
const outPath = deliverableXlsxPath(cwd, screen, locale);
|
|
128
|
+
const outPath = deliverableXlsxPath(cwd, screen, locale, opts);
|
|
128
129
|
const outDir = path.dirname(outPath);
|
|
129
130
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
130
131
|
const buffer = await wb.xlsx.writeBuffer();
|
|
@@ -84,12 +84,14 @@ export function evaluateGraph(
|
|
|
84
84
|
* rules of its own: each offered step's node says what a person runs (its `human` hook), and a decide
|
|
85
85
|
* step without one is its own command. `cut`: an open blocking node ended the walk, so only what does
|
|
86
86
|
* not depend on it is offered (a hard verdict that surfaces past the cut), and the board's asks that
|
|
87
|
-
* need the unit past it (sign-off, accept-in-run) wait; `heldBy` is that node's title
|
|
87
|
+
* need the unit past it (sign-off, accept-in-run) wait; `heldBy` is that node's title and `heldByCommand`
|
|
88
|
+
* its offered step's command (#966: "More appear once …" names what to run).
|
|
88
89
|
*/
|
|
89
|
-
export function evaluateHumanActions(nodes: readonly NodeDecl[], s: UnitState, tcStates?: TestCaseState[]): { actions: HumanAction[]; cut: boolean; heldBy?: string } {
|
|
90
|
+
export function evaluateHumanActions(nodes: readonly NodeDecl[], s: UnitState, tcStates?: TestCaseState[]): { actions: HumanAction[]; cut: boolean; heldBy?: string; heldByCommand?: string } {
|
|
90
91
|
const { offered, cut, cutBy } = walkFrontier(nodes, s, tcStates, {});
|
|
91
92
|
const actions = offered.flatMap(({ node, step, cases }) => (node.human
|
|
92
93
|
? node.human(s, step, cases)
|
|
93
94
|
: step.kind === 'decide' ? [{ command: step.command, reason: step.because, kind: 'decide' as const }] : []));
|
|
94
|
-
|
|
95
|
+
const heldByCommand = cutBy ? offered.find((o) => o.node === cutBy)?.step.command : undefined;
|
|
96
|
+
return { actions, cut, ...(cutBy ? { heldBy: cutBy.title } : {}), ...(heldByCommand ? { heldByCommand } : {}) };
|
|
95
97
|
}
|
|
@@ -9,6 +9,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
9
9
|
import { featureFilesFor } from '../harness/unit-paths';
|
|
10
10
|
import { readTextFile } from '../harness/read-text';
|
|
11
11
|
import { loadPlaywrightReport } from '../exporters/playwright-report-parser';
|
|
12
|
+
import { isDraftDeliverable } from '../exporters/csv-exporter';
|
|
12
13
|
|
|
13
14
|
/** `<f>-test-result[.<os>][.<env>].json` — a mobile run writes the OS into the name, an env run the env (#868). */
|
|
14
15
|
export const RESULT_FILE = /-test-result(?:\.[^.]+)*\.json$/;
|
|
@@ -72,6 +73,7 @@ export function countFailedResults(resultFiles: string[]): number {
|
|
|
72
73
|
|
|
73
74
|
export function hasDeliverableFor(projectRoot: string, slug: string): boolean {
|
|
74
75
|
try {
|
|
75
|
-
|
|
76
|
+
// A `--preview` DRAFT is not a deliverable (#965).
|
|
77
|
+
return fs.readdirSync(path.join(projectRoot, 'qa', 'deliverables')).some((f) => f.startsWith(`${slug}-`) && !isDraftDeliverable(f));
|
|
76
78
|
} catch { return false; } // nothing exported yet
|
|
77
79
|
}
|
|
@@ -58,6 +58,8 @@ function provenanceField(r: Record<string, unknown>, key: 'sungenVersion' | 'cat
|
|
|
58
58
|
const writtenBy = (r: Record<string, unknown>): string | null => provenanceField(r, 'sungenVersion');
|
|
59
59
|
|
|
60
60
|
export interface RulesStamp { catalogHash: string | null; rulesHash: string | null }
|
|
61
|
+
/** rulesProblem's answer for a report that records no rules (`provenance.catalogHash` / `rulesHash`): an older sungen wrote it. */
|
|
62
|
+
export const NO_RULES_STAMP = 'audited by an older sungen, which did not record the rules it scored with';
|
|
61
63
|
/** What getProvenance writes when it could not read a catalog — never a match, on either side. */
|
|
62
64
|
const UNKNOWN = 'unknown';
|
|
63
65
|
|
|
@@ -67,7 +69,7 @@ const UNKNOWN = 'unknown';
|
|
|
67
69
|
* unknown == unknown proves nothing.
|
|
68
70
|
*/
|
|
69
71
|
export function rulesProblem(was: RulesStamp, now: RulesStamp): string | null {
|
|
70
|
-
if (!was.catalogHash || !was.rulesHash) return
|
|
72
|
+
if (!was.catalogHash || !was.rulesHash) return NO_RULES_STAMP;
|
|
71
73
|
if ([was.catalogHash, was.rulesHash].includes(UNKNOWN)) return 'the rules that scored it are unknown (provenance says "unknown")';
|
|
72
74
|
if (!now.catalogHash || !now.rulesHash || [now.catalogHash, now.rulesHash].includes(UNKNOWN)) return 'this sungen cannot read its own rules (catalog or rules hash unknown), so it trusts no report';
|
|
73
75
|
if (was.catalogHash === now.catalogHash && was.rulesHash === now.rulesHash) return null;
|
|
@@ -162,7 +164,10 @@ export function verifyAuditReport(projectRoot: string, unit: string): AuditVerif
|
|
|
162
164
|
}
|
|
163
165
|
const stale: AuditVerification['problems'] = [];
|
|
164
166
|
const rules = otherRules(r);
|
|
165
|
-
|
|
167
|
+
// A report from before the rules stamp reads in plain words, with the one command that clears it (#966).
|
|
168
|
+
if (rules) stale.push({ code: 'rules-changed', message: rules === NO_RULES_STAMP
|
|
169
|
+
? `audited by an older sungen${writtenBy(r) ? ` (${writtenBy(r)})` : ''} — run \`sungen audit --screen ${unit}\` once`
|
|
170
|
+
: `${writtenBy(r) ? `written by sungen ${writtenBy(r)}, ` : ''}${rules}` });
|
|
166
171
|
if (r.decisionsHash !== current) stale.push({ code: 'decisions-changed', message: 'the gate-override decisions changed since this audit' });
|
|
167
172
|
if (!unitDir) return result('invalid', [{ code: 'unit-unresolved', message: `"${unit}" does not name exactly one unit, so the audit's inputs cannot be checked` }]);
|
|
168
173
|
if (r.inputsHash !== auditInputsHash(unitDir, unit, r.decisionsHash as string)) {
|
package/src/harness/next-step.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface UnitState {
|
|
|
106
106
|
* are listed (sign-off, accept-in-run and the first contract approval wait). Absent when nothing cuts.
|
|
107
107
|
*/
|
|
108
108
|
humanActionsHeldBy?: string;
|
|
109
|
+
/** The canonical command of that blocking step (#966) — what clears the hold. */
|
|
110
|
+
humanActionsHeldByCommand?: string;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export interface ReadUnitStateOptions {
|
|
@@ -188,6 +190,7 @@ export function readUnitState(
|
|
|
188
190
|
state.humanActions = [...graph.actions, ...boardHumanActions(projectRoot, unitDir, state, graph.cut)]
|
|
189
191
|
.filter((a) => !seen.has(a.command) && Boolean(seen.add(a.command)));
|
|
190
192
|
if (graph.heldBy) state.humanActionsHeldBy = graph.heldBy;
|
|
193
|
+
if (graph.heldByCommand) state.humanActionsHeldByCommand = graph.heldByCommand;
|
|
191
194
|
return state;
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* commander's preAction, so a usage error — which never reaches an action — and `main().catch`
|
|
11
11
|
* ended in silence. The state now starts the moment parsing does; the action only refines it.
|
|
12
12
|
*/
|
|
13
|
-
import { sungenInvocation,
|
|
13
|
+
import { sungenInvocation, withProjectInvocationInText } from '../cli/project-sungen-invocation';
|
|
14
14
|
import { waitingForYouLines } from '../cli/waiting-for-you-block';
|
|
15
15
|
import { resolveUnitCandidates } from '../cli/resolve-unit';
|
|
16
16
|
import { readUnitState, NextStep, UnitState } from '../harness/next-step';
|
|
@@ -88,10 +88,14 @@ export function attachCliHandBack(argv: string[], cwd: string): { onAction: (nam
|
|
|
88
88
|
|
|
89
89
|
const MARK: Record<NextStep['kind'], string> = { do: '→', blocked: '⛔', optional: '○', decide: '⚖' };
|
|
90
90
|
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* `sungen next` rendered compactly for an agent's context; commands — the step's own and those quoted in
|
|
93
|
+
* its reason (#966) — as `root` runs sungen, the way `sungen next` prints them.
|
|
94
|
+
*/
|
|
92
95
|
export function formatSteps(steps: NextStep[], max = 4, root?: string): string[] {
|
|
96
|
+
const asRun = (text: string): string => (root ? withProjectInvocationInText(text, root) : text);
|
|
93
97
|
return steps.slice(0, max).map((st) =>
|
|
94
|
-
` ${MARK[st.kind]} ${
|
|
98
|
+
` ${MARK[st.kind]} ${asRun(st.command)}${st.kind === 'decide' ? ' (a QA decision — ask, do not run)' : ''}\n ${asRun(st.because)}`);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/** The unit a session is on (a named one when it exists, else the nearest) and its derived state. */
|
|
@@ -109,6 +113,6 @@ export function run(input: HookInput, ctx: HookContext): HookResult {
|
|
|
109
113
|
return {
|
|
110
114
|
status: 'context',
|
|
111
115
|
output: [`${handBackLine(found.unit.name, sungenInvocation(found.root))} — derived from what the project holds now:`, ...formatSteps(found.state.steps, 4, found.root),
|
|
112
|
-
...waitingForYouLines(found.state.humanActions, found.root, found.state.humanActionsHeldBy)].join('\n'),
|
|
116
|
+
...waitingForYouLines(found.state.humanActions, found.root, found.state.humanActionsHeldBy, found.state.humanActionsHeldByCommand)].join('\n'),
|
|
113
117
|
};
|
|
114
118
|
}
|
|
@@ -50,7 +50,7 @@ export const sessionStartUnitStateHook: SungenHook = {
|
|
|
50
50
|
state.audit ? `Audit: ${state.audit.overall}/10 [${state.audit.gateStatus}] · ${state.audit.findings.length} finding(s).` : '',
|
|
51
51
|
`Next (\`${sungen} next ${unit.name}\`, derived from the project state):`,
|
|
52
52
|
...formatSteps(state.steps, 4, root),
|
|
53
|
-
...waitingForYouLines(state.humanActions, root, state.humanActionsHeldBy),
|
|
53
|
+
...waitingForYouLines(state.humanActions, root, state.humanActionsHeldBy, state.humanActionsHeldByCommand),
|
|
54
54
|
input.source === 'compact' ? 'Context was compacted: re-read the unit\'s files before acting on them.' : '',
|
|
55
55
|
others > 0 ? `Working on another unit? Ask \`${sungen} next <unit>\` (or \`${sungen} next\` for all).` : '',
|
|
56
56
|
];
|