@opengsd/gsd-core 1.7.0-rc.5 → 1.7.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-statusline.js +164 -14
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -305,8 +305,8 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry,
|
|
|
305
305
|
// surface-path agents lack path-prefix rewrites and Co-Authored-By trailers,
|
|
306
306
|
// diverging from a fresh install.
|
|
307
307
|
const _homedirFn = opts?.homedir ?? (() => node_os_1.default.homedir());
|
|
308
|
-
const _resolvedTarget = node_path_1.default.resolve(layout.configDir)
|
|
309
|
-
const _homeDir =
|
|
308
|
+
const _resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(layout.configDir));
|
|
309
|
+
const _homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(_homedirFn());
|
|
310
310
|
const _isGlobal = (layout.scope ?? 'global') === 'global';
|
|
311
311
|
const _isOpencode = layout.runtime === 'opencode';
|
|
312
312
|
const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
|
|
@@ -472,10 +472,14 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
|
|
|
472
472
|
// Normalize: allow legacy string context for backward-compat with internal callers
|
|
473
473
|
const kindName = (typeof kind === 'string') ? kind : kind.kind;
|
|
474
474
|
const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'gsd-';
|
|
475
|
-
// #1575:
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
475
|
+
// #1575 / #2103: agent files are renamed .md -> <agentFileExtension> at copy
|
|
476
|
+
// time when the runtime's descriptor declares hostBehaviors.agentFileExtension
|
|
477
|
+
// (e.g. copilot's '.agent.md'), mirroring install-engine.cts's staged-copy
|
|
478
|
+
// loop (`_copyStaged`) — ONE descriptor read shared by both surfaces instead
|
|
479
|
+
// of a duplicated hardcoded `runtime === 'copilot'` literal. Other runtimes
|
|
480
|
+
// (no agentFileExtension declared) keep the staged filename verbatim.
|
|
481
|
+
const _agentExt = runtime ? runtimeArtifactConversion.agentFileExtensionFor(runtime) : undefined;
|
|
482
|
+
const isRenamedAgents = !!_agentExt && kindName === 'agents';
|
|
479
483
|
if (kindName === 'skills') {
|
|
480
484
|
// Skills kind: work with directories, not files.
|
|
481
485
|
// Each staged entry is a directory named ${prefix}${stem}.
|
|
@@ -510,8 +514,8 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
|
|
|
510
514
|
const stagedFiles = node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
|
|
511
515
|
const stagedDestNames = new Set();
|
|
512
516
|
for (const file of stagedFiles) {
|
|
513
|
-
const destName =
|
|
514
|
-
? file.replace(/\.md$/,
|
|
517
|
+
const destName = isRenamedAgents
|
|
518
|
+
? file.replace(/\.md$/, _agentExt)
|
|
515
519
|
: (kindName === 'agents' || namespacedByDir)
|
|
516
520
|
? file
|
|
517
521
|
: `${kindPrefix}${file.slice(0, -3)}.md`;
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
14
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
16
|
const ioMod = require("./io.cjs");
|
|
16
17
|
const { output, error } = ioMod;
|
|
@@ -85,7 +86,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
|
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
const padded = normalizePhaseName(options.phase);
|
|
88
|
-
const today =
|
|
89
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
89
90
|
const phaseName = options.name || phaseInfo.phase_name || 'Unnamed';
|
|
90
91
|
const phaseSlug = phaseInfo.phase_slug || generateSlugInternal(phaseName);
|
|
91
92
|
const phaseId = `${padded}-${phaseSlug}`;
|
package/gsd-core/bin/lib/uat.cjs
CHANGED
|
@@ -21,6 +21,9 @@ const { output, error } = io;
|
|
|
21
21
|
const markdownSectionizer = require("./markdown-sectionizer.cjs");
|
|
22
22
|
const { collectSection, tokenizeHeadings } = markdownSectionizer;
|
|
23
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const markdownTable = require("./markdown-table.cjs");
|
|
25
|
+
const { splitTableRow } = markdownTable;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
27
|
const roadmapParser = require("./roadmap-parser.cjs");
|
|
25
28
|
const { getMilestonePhaseFilter } = roadmapParser;
|
|
26
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -292,24 +295,67 @@ function parseVerificationItems(content, status) {
|
|
|
292
295
|
// Use the seam to locate the ## Human Verification section (ADR-1372 T5).
|
|
293
296
|
const hvSection = collectSection(content, (h) => /^human\s+verification/i.test(h.text) && h.level === 2, { levelBounded: true });
|
|
294
297
|
if (hvSection) {
|
|
298
|
+
// #2245 review Fix 3: reverted to the pre-Phase-4 (HEAD 2cbf18642)
|
|
299
|
+
// implementation. The live Human Verification section is NOT a strict
|
|
300
|
+
// GFM table — the planner/verifier templates mix table rows, numbered
|
|
301
|
+
// items, and bullet items in the same section (and a `### N.` heading
|
|
302
|
+
// format is common too), so a table-XOR-list read (parse a table, and
|
|
303
|
+
// if it parses, suppress numbered/bullet items entirely) silently
|
|
304
|
+
// dropped items on any mixed or malformed section: a malformed
|
|
305
|
+
// `| N | … |` table with no valid header/delimiter yielded ZERO items
|
|
306
|
+
// instead of reading the rows positionally. This per-line scan reads
|
|
307
|
+
// table rows AND numbered items AND bullet items as a UNION (whichever
|
|
308
|
+
// pattern a given line matches), exactly like OLD, and reads
|
|
309
|
+
// `| N | desc |` rows even without a valid table header/delimiter.
|
|
310
|
+
//
|
|
311
|
+
// #2245 audit: the table-row branch's CELL SPLIT is name/position-
|
|
312
|
+
// addressed via `splitTableRow` (escape-aware, canonical) instead of a
|
|
313
|
+
// hand-rolled pipe regex — candidacy itself is decided WITHOUT a table
|
|
314
|
+
// regex (a leading `|` plus a purely-numeric first cell), so this no
|
|
315
|
+
// longer needs an allow-adhoc-markdown suppression at all.
|
|
295
316
|
const lines = hvSection.body.split('\n');
|
|
296
317
|
for (const line of lines) {
|
|
297
|
-
|
|
298
|
-
|
|
318
|
+
const trimmedLine = line.trim();
|
|
319
|
+
// Match table rows: | N | description | ... — candidacy requires a
|
|
320
|
+
// leading pipe and a purely-numeric first cell (mirrors what the old
|
|
321
|
+
// regex effectively required: a "|digit|" cell immediately followed
|
|
322
|
+
// by more content), with at least 2 physical cells so a bare "| N |"
|
|
323
|
+
// with nothing after it is NOT treated as a row.
|
|
324
|
+
//
|
|
325
|
+
// #2245 review Fix 9: this is NOT the same as OLD for a row whose
|
|
326
|
+
// ONLY content past the digit cell is trailing whitespace (e.g.
|
|
327
|
+
// "| N | ", no second delimiting `|`). OLD's `([^|]+)` regex ran
|
|
328
|
+
// against the RAW (untrimmed) line and its `\s*` would backtrack to
|
|
329
|
+
// let `[^|]+` swallow that trailing whitespace, so OLD matched and
|
|
330
|
+
// pushed an item with an EMPTY (`.trim()`-collapsed) name. Here,
|
|
331
|
+
// `trimmedLine = line.trim()` strips that trailing whitespace BEFORE
|
|
332
|
+
// `splitTableRow` ever sees it, collapsing the line to a single cell
|
|
333
|
+
// (`candidateCells.length === 1`), which fails the `>= 2` check —
|
|
334
|
+
// the item is silently dropped instead. A real, acceptable behaviour
|
|
335
|
+
// change (an empty-named UAT item is not useful either way), but the
|
|
336
|
+
// two implementations are NOT equivalent on this input.
|
|
337
|
+
let tableCells = null;
|
|
338
|
+
if (trimmedLine.startsWith('|')) {
|
|
339
|
+
const candidateCells = splitTableRow(trimmedLine);
|
|
340
|
+
if (candidateCells.length >= 2 && /^\d+$/.test(candidateCells[0])) {
|
|
341
|
+
tableCells = candidateCells;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
299
344
|
// Match bullet items: - description
|
|
300
345
|
const bulletMatch = line.match(/^[-*]\s+(.+)/);
|
|
301
346
|
// Match numbered items: 1. description
|
|
302
347
|
const numberedMatch = line.match(/^(\d+)\.\s+(.+)/);
|
|
303
|
-
if (
|
|
348
|
+
if (tableCells) {
|
|
304
349
|
// Skip rows that already have a passing result (PASS, pass, resolved, etc.)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
350
|
+
// — checked over every cell AFTER the description column, mirroring
|
|
351
|
+
// OLD's rowRemainder scan (which only ever saw cells past the
|
|
352
|
+
// description, the description itself having already been consumed).
|
|
353
|
+
const hasPassResult = tableCells.slice(2).some(c => /^pass$/i.test(c) || /^resolved$/i.test(c));
|
|
308
354
|
if (hasPassResult)
|
|
309
355
|
continue;
|
|
310
356
|
items.push({
|
|
311
|
-
test: parseInt(
|
|
312
|
-
name:
|
|
357
|
+
test: parseInt(tableCells[0], 10),
|
|
358
|
+
name: tableCells[1] ?? '',
|
|
313
359
|
result: 'human_needed',
|
|
314
360
|
category: 'human_uat',
|
|
315
361
|
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* UI-consideration probe — the THIRD adapter of the probe-core resolution model
|
|
4
|
+
* (ADR-457 build model; ADR-550 Decision 7 seam; #1867).
|
|
5
|
+
*
|
|
6
|
+
* The generic resolution lifecycle, the status×verification re-cut, `validateResolution`,
|
|
7
|
+
* `validateRequirement`, the `analyzeCoverage` merge/rollup/orphan-reject engine, and the
|
|
8
|
+
* `runProbeCli` scaffold all live in `src/probe-core.cts`. This module keeps ONLY the
|
|
9
|
+
* UI-specific cluster: the six element kinds, the closed 8-category shape-rooted UI state
|
|
10
|
+
* taxonomy, element classification, consideration proposal, and the `{ explicit, backstop }`
|
|
11
|
+
* verification validators — mirroring `edge-probe` on the UI element/state axis.
|
|
12
|
+
*
|
|
13
|
+
* MIXED-axis boundary (spike verdict, ADR-550 pattern): this compiled taxonomy covers ONLY the
|
|
14
|
+
* finite, project-independent shape-rooted *content/robustness* states (empty/loading/error/…).
|
|
15
|
+
* Open, domain-specific UX considerations (real-time/offline, deep a11y/WCAG breadth, i18n/RTL
|
|
16
|
+
* depth, emerging interaction paradigms) are prose-owned in `references/domain-probes.md`, NOT
|
|
17
|
+
* here — forcing them into a closed compiled taxonomy is the wrong model.
|
|
18
|
+
*
|
|
19
|
+
* Authored as strict TypeScript (`src/ui-consideration-probe.cts`) and compiled by
|
|
20
|
+
* `tsc -p tsconfig.build.json` to the gitignored runtime artifact
|
|
21
|
+
* `gsd-core/bin/lib/ui-consideration-probe.cjs`. Do NOT hand-write the `.cjs`; it is emitted.
|
|
22
|
+
* Tests `require()` the built artifact; `pretest` runs `build:lib` first.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.UI_VALIDATORS = exports.UNCLASSIFIED_CATEGORY = exports.UI_TAXONOMY = exports.VALID_ELEMENT_KINDS = exports.UI_CUES = void 0;
|
|
26
|
+
exports.classifyElement = classifyElement;
|
|
27
|
+
exports.applicableCategories = applicableCategories;
|
|
28
|
+
exports.validateRequirement = validateRequirement;
|
|
29
|
+
exports.validateResolution = validateResolution;
|
|
30
|
+
exports.proposeConsiderations = proposeConsiderations;
|
|
31
|
+
exports.analyzeCoverage = analyzeCoverage;
|
|
32
|
+
exports.proposeElements = proposeElements;
|
|
33
|
+
exports.autoResolve = autoResolve;
|
|
34
|
+
const probe_core_cjs_1 = require("./probe-core.cjs");
|
|
35
|
+
/**
|
|
36
|
+
* Word-boundary cues mapping element prose -> UI element kind.
|
|
37
|
+
* Heuristic and intentionally lossy; an authored `elements` array overrides it. Every pattern is a
|
|
38
|
+
* flat linear `\b(a|b|c)\b` alternation with NO nested/overlapping quantifiers (no catastrophic
|
|
39
|
+
* backtracking — mirrors SHAPE_CUES).
|
|
40
|
+
*/
|
|
41
|
+
exports.UI_CUES = {
|
|
42
|
+
'form': /\b(forms?|inputs?|fields?|submit|validation|validate|password|email|checkbox|radio|textarea)\b/i,
|
|
43
|
+
'list-collection': /\b(lists?|listing|tables?|grids?|collections?|rows?|items?|cards?|feed|results?)\b/i,
|
|
44
|
+
'nav': /\b(nav|navigation|menus?|tabs?|breadcrumbs?|pagination|sidebars?)\b/i,
|
|
45
|
+
'media': /\b(images?|img|videos?|avatars?|thumbnails?|photos?|gallery|icons?)\b/i,
|
|
46
|
+
'interactive-control': /\b(buttons?|toggles?|switch|switches|dropdowns?|sliders?|controls?|pickers?)\b/i,
|
|
47
|
+
'static-content': /\b(labels?|headings?|titles?|paragraphs?|copy|descriptions?|text)\b/i,
|
|
48
|
+
};
|
|
49
|
+
/** The locked element vocabulary — exactly the keys of UI_CUES (single source of truth). */
|
|
50
|
+
exports.VALID_ELEMENT_KINDS = new Set(Object.keys(exports.UI_CUES));
|
|
51
|
+
/** Detect which element kinds a description's prose matches (heuristic). */
|
|
52
|
+
function classifyElement(text) {
|
|
53
|
+
const kinds = [];
|
|
54
|
+
const subject = String(text == null ? '' : text);
|
|
55
|
+
for (const kind of Object.keys(exports.UI_CUES)) {
|
|
56
|
+
if (exports.UI_CUES[kind].test(subject))
|
|
57
|
+
kinds.push(kind);
|
|
58
|
+
}
|
|
59
|
+
return kinds;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Closed taxonomy of 8 shape-rooted UI *content/robustness* state categories. `elements` lists
|
|
63
|
+
* which element kinds make the category relevant. These ids are the CLOSED/compiled subset — the
|
|
64
|
+
* open UX subset (real-time/offline, deep a11y, i18n/RTL depth) is prose-owned in
|
|
65
|
+
* `references/domain-probes.md` and deliberately absent here (D-02).
|
|
66
|
+
*/
|
|
67
|
+
exports.UI_TAXONOMY = [
|
|
68
|
+
{ id: 'empty', name: 'Empty / no data', elements: ['form', 'list-collection', 'media'], consideration: 'What is shown when there is no data — zero items, an unfilled form, or absent media?' },
|
|
69
|
+
{ id: 'loading', name: 'Loading / in-flight', elements: ['form', 'list-collection', 'media', 'nav'], consideration: 'What is shown while data or content is still loading (skeleton, spinner, progressive reveal)?' },
|
|
70
|
+
{ id: 'error', name: 'Error / failure', elements: ['form', 'list-collection', 'media', 'nav'], consideration: 'What is shown when the load or submit fails (message, retry affordance, partial fallback)?' },
|
|
71
|
+
{ id: 'populated', name: 'Populated / happy path', elements: ['list-collection', 'media'], consideration: 'What does the normal populated (happy-path) state look like at a typical volume of content?' },
|
|
72
|
+
{ id: 'partial', name: 'Partial / incomplete', elements: ['form', 'list-collection'], consideration: 'What is shown for partial or incomplete data — some fields or rows present, others missing?' },
|
|
73
|
+
{ id: 'overflow', name: 'Overflow / truncation', elements: ['list-collection', 'nav', 'static-content'], consideration: 'What happens when content exceeds its container — scroll, clip, wrap, or truncate?' },
|
|
74
|
+
{ id: 'zero-one-many', name: 'Zero / one / many', elements: ['list-collection'], consideration: 'How does the layout read at zero, one, and many items (singular vs plural copy, spacing)?' },
|
|
75
|
+
{ id: 'long-text', name: 'Long text', elements: ['form', 'static-content', 'interactive-control', 'nav'], consideration: 'What happens with unusually long text — truncation, wrapping, ellipsis, or reflow?' },
|
|
76
|
+
];
|
|
77
|
+
/** Return taxonomy category ids whose applicable element kinds intersect the input set. */
|
|
78
|
+
function applicableCategories(kinds) {
|
|
79
|
+
const set = new Set(kinds);
|
|
80
|
+
return exports.UI_TAXONOMY.filter((c) => c.elements.some((k) => set.has(k))).map((c) => c.id);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pseudo-category for an element whose prose matched NO element cue (#1110). It is a soft
|
|
84
|
+
* "review manually" signal, NOT a 9th taxonomy category: it stays out of `UI_TAXONOMY` (the closed
|
|
85
|
+
* eight) and only joins `UI_VALIDATORS.categories` so `analyzeCoverage` accepts the item.
|
|
86
|
+
*/
|
|
87
|
+
exports.UNCLASSIFIED_CATEGORY = 'unclassified';
|
|
88
|
+
const UNCLASSIFIED_PROBE = 'unclassified — review manually';
|
|
89
|
+
/**
|
|
90
|
+
* The UI adapter's injected runtime validators (ADR-550 #5). `categories` is the closed taxonomy
|
|
91
|
+
* plus the unclassified soft-signal; both verification tiers require a non-empty `resolution` so
|
|
92
|
+
* plan-phase has a criterion to lift. NOTE the probe-core Validators field is `verification`
|
|
93
|
+
* (SINGULAR); CONTEXT.md D-05's `verifications` is a paraphrase typo, not the real field name.
|
|
94
|
+
*/
|
|
95
|
+
exports.UI_VALIDATORS = {
|
|
96
|
+
categories: [...exports.UI_TAXONOMY.map((c) => c.id), exports.UNCLASSIFIED_CATEGORY],
|
|
97
|
+
verification: ['explicit', 'backstop'],
|
|
98
|
+
requiredFieldsByVerification: { explicit: ['resolution'], backstop: ['resolution'] },
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Validate a single element — the generic id/text checks (probe-core) plus the UI adapter's
|
|
102
|
+
* `elements`-must-be-an-array check. The `text` prose is REQUIRED (it is the classification
|
|
103
|
+
* signal), so reject a missing/empty `text` when no authored `elements` override is present.
|
|
104
|
+
* Without this, a `{ id }` element classifies to zero kinds → zero considerations → it is silently
|
|
105
|
+
* DROPPED from coverage. An explicit `elements` array (including `[]` for "no applicable
|
|
106
|
+
* categories") is the legitimate way to opt out of prose classification.
|
|
107
|
+
*/
|
|
108
|
+
function validateRequirement(element) {
|
|
109
|
+
(0, probe_core_cjs_1.validateRequirement)(element);
|
|
110
|
+
const r = element;
|
|
111
|
+
if (r.elements != null && !Array.isArray(r.elements)) {
|
|
112
|
+
throw new Error(`element ${element.id} elements must be an array when present`);
|
|
113
|
+
}
|
|
114
|
+
if (r.elements == null && !(typeof r.text === 'string' && r.text.trim())) {
|
|
115
|
+
throw new Error(`element ${element.id} text must be a non-empty string when no elements override is provided`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Validate a UI-consideration resolution against the UI verification vocabulary (delegated, D-06). */
|
|
119
|
+
function validateResolution(resolution) {
|
|
120
|
+
return (0, probe_core_cjs_1.validateResolution)(resolution, exports.UI_VALIDATORS);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Propose candidate considerations for an element. Uses authored `elements` when present, else
|
|
124
|
+
* classifies from prose. Every proposed consideration starts unresolved (verification null); the
|
|
125
|
+
* taxonomy entry's `consideration` question is carried in the item's `probe` field.
|
|
126
|
+
*/
|
|
127
|
+
function proposeConsiderations(element) {
|
|
128
|
+
validateRequirement(element);
|
|
129
|
+
let kinds;
|
|
130
|
+
if (Array.isArray(element.elements)) {
|
|
131
|
+
// Fail closed: an authored array must contain only locked element kinds. A non-empty but
|
|
132
|
+
// invalid array would otherwise intersect no category and silently suppress every probe — the
|
|
133
|
+
// gate reads green while nothing was checked. An empty array stays a valid "no applicable
|
|
134
|
+
// categories" override (silent opt-out).
|
|
135
|
+
for (const k of element.elements) {
|
|
136
|
+
if (typeof k !== 'string' || !exports.VALID_ELEMENT_KINDS.has(k)) {
|
|
137
|
+
throw new Error(`invalid element kind ${JSON.stringify(k)} for element ${element.id} — must be one of: ${[...exports.VALID_ELEMENT_KINDS].join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
kinds = element.elements;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
kinds = classifyElement(element.text);
|
|
144
|
+
if (kinds.length === 0) {
|
|
145
|
+
// Prose present but no element cue matched. Do NOT silently drop it (#1110): a UI element
|
|
146
|
+
// whose phrasing missed every cue would otherwise vanish from coverage with no signal — the
|
|
147
|
+
// exact blind spot this probe exists to catch. Surface ONE soft, dismissible "unclassified —
|
|
148
|
+
// review manually" candidate. The explicit `elements: []` opt-out (above) stays silent.
|
|
149
|
+
return [{
|
|
150
|
+
requirement_id: element.id,
|
|
151
|
+
category: exports.UNCLASSIFIED_CATEGORY,
|
|
152
|
+
status: 'unresolved',
|
|
153
|
+
verification: null,
|
|
154
|
+
resolution: null,
|
|
155
|
+
reason: null,
|
|
156
|
+
probe: UNCLASSIFIED_PROBE,
|
|
157
|
+
}];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return applicableCategories(kinds).map((catId) => {
|
|
161
|
+
const cat = exports.UI_TAXONOMY.find((c) => c.id === catId);
|
|
162
|
+
return {
|
|
163
|
+
requirement_id: element.id,
|
|
164
|
+
category: catId,
|
|
165
|
+
status: 'unresolved',
|
|
166
|
+
verification: null,
|
|
167
|
+
resolution: null,
|
|
168
|
+
reason: null,
|
|
169
|
+
probe: cat ? cat.consideration : '',
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Propose considerations for every element (deterministic propose), then delegate the
|
|
175
|
+
* merge/rollup/orphan-reject to probe-core. UI-specific pre-checks: elements must be an array,
|
|
176
|
+
* element ids must be unique. Throws on any invalid resolution.
|
|
177
|
+
*/
|
|
178
|
+
function analyzeCoverage(elements, resolutions = []) {
|
|
179
|
+
if (!Array.isArray(elements)) {
|
|
180
|
+
throw new Error('elements must be an array');
|
|
181
|
+
}
|
|
182
|
+
const items = [];
|
|
183
|
+
const seenIds = new Set();
|
|
184
|
+
for (const el of elements) {
|
|
185
|
+
validateRequirement(el);
|
|
186
|
+
if (seenIds.has(el.id)) {
|
|
187
|
+
throw new Error(`duplicate element id ${JSON.stringify(el.id)}`);
|
|
188
|
+
}
|
|
189
|
+
seenIds.add(el.id);
|
|
190
|
+
for (const consideration of proposeConsiderations(el))
|
|
191
|
+
items.push(consideration);
|
|
192
|
+
}
|
|
193
|
+
return (0, probe_core_cjs_1.analyzeCoverage)(items, resolutions, exports.UI_VALIDATORS);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Build the propose-then-confirm view for every element (WIRE-01). Deterministic: a pure function
|
|
197
|
+
* of the input array (no Date/random/iteration-order surprise), so re-running the probe on an
|
|
198
|
+
* unchanged UI-SPEC yields byte-identical rows (the idempotency substrate WIRE-02 relies on). An
|
|
199
|
+
* aggregating VIEW over the existing Phase-1 functions — it adds no new classification logic.
|
|
200
|
+
*
|
|
201
|
+
* `unclassified` is true ONLY when prose classified to zero cues (#1110); an explicit `elements: []`
|
|
202
|
+
* opt-out stays silent (`unclassified: false`, empty considerations), matching proposeConsiderations.
|
|
203
|
+
*/
|
|
204
|
+
function proposeElements(elements) {
|
|
205
|
+
return elements.map((el) => {
|
|
206
|
+
validateRequirement(el);
|
|
207
|
+
const considerations = proposeConsiderations(el);
|
|
208
|
+
const kinds = Array.isArray(el.elements)
|
|
209
|
+
? el.elements // already validated inside proposeConsiderations
|
|
210
|
+
: classifyElement(el.text);
|
|
211
|
+
const unclassified = !Array.isArray(el.elements) && kinds.length === 0;
|
|
212
|
+
const categories = unclassified ? [] : applicableCategories(kinds);
|
|
213
|
+
return { id: el.id, kinds, categories, considerations, unclassified };
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The deterministic `--auto` resolution FLOOR (WIRE-01, SC2). For each proposed consideration:
|
|
218
|
+
* - an `unclassified` item stays `unresolved` — NEVER auto-backstopped (a missing cue is not
|
|
219
|
+
* evidence a consideration applies, #1110);
|
|
220
|
+
* - every applicable item auto-resolves to a conservative `backstop` (carrying the taxonomy
|
|
221
|
+
* question as its `resolution` so probe-core's "backstop requires a resolution" check passes).
|
|
222
|
+
* - it NEVER emits `dismissed` under any branch — a wrong auto-dismissal is the exact silent
|
|
223
|
+
* failure this probe eliminates (the never-dismiss invariant, asserted on the typed return).
|
|
224
|
+
*
|
|
225
|
+
* This is the CODE floor only. It mirrors spec-phase.md Step 5.5's prose `--auto` rule
|
|
226
|
+
* (auto-`covered` where a defensible acceptance criterion can be written, else auto-`backstop`,
|
|
227
|
+
* never auto-`dismiss`) but deliberately keeps the covered-vs-backstop JUDGMENT in the ui-phase
|
|
228
|
+
* workflow (an LLM MAY upgrade an item to `explicit`/covered when it can write a real acceptance
|
|
229
|
+
* criterion). Encoding the never-dismiss FLOOR in code is what makes the invariant unit-testable;
|
|
230
|
+
* the covered-upgrade stays prose because "a defensible criterion exists" is not a code predicate.
|
|
231
|
+
* Keep the two in sync: if spec-phase's `--auto` policy changes, revisit this floor.
|
|
232
|
+
*/
|
|
233
|
+
function autoResolve(items) {
|
|
234
|
+
return items.map((item) => {
|
|
235
|
+
if (item.category === exports.UNCLASSIFIED_CATEGORY) {
|
|
236
|
+
return { requirement_id: item.requirement_id, category: item.category, status: 'unresolved', verification: null, resolution: null, reason: null };
|
|
237
|
+
}
|
|
238
|
+
return { requirement_id: item.requirement_id, category: item.category, status: 'resolved', verification: 'backstop', resolution: item.probe, reason: null };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/*
|
|
242
|
+
* CLI entry (invokable surface): `ui-consideration-probe.cjs <elements.json> [resolutions.json]`.
|
|
243
|
+
* The generic I/O plumbing (parse, fail-closed exit 2, pretty-JSON out) lives in probe-core's
|
|
244
|
+
* `runProbeCli`; this adapter supplies its `analyzeCoverage`. Guarded by `require.main === module`
|
|
245
|
+
* so it runs only when the compiled `.cjs` is executed directly.
|
|
246
|
+
*/
|
|
247
|
+
if (require.main === module) {
|
|
248
|
+
(0, probe_core_cjs_1.runProbeCli)((elements, resolutions) => analyzeCoverage(elements, resolutions), { usage: 'ui-consideration-probe.cjs <elements.json> [resolutions.json]' });
|
|
249
|
+
}
|
|
@@ -67,14 +67,36 @@ function checkUiPresence(text) {
|
|
|
67
67
|
}
|
|
68
68
|
// Normalise CRLF so the pattern sees consistent line boundaries.
|
|
69
69
|
const normalised = text.replace(/\r\n/g, '\n');
|
|
70
|
+
// #2150: an explicit `**UI hint**: yes|no` metadata line is the author's
|
|
71
|
+
// authoritative declaration of whether the phase has a UI surface — progress.md
|
|
72
|
+
// and new-project.md already parse this line (`UI hint.*yes`). The bare token
|
|
73
|
+
// `UI` in the line itself must not count as a UI indicator, and the declaration
|
|
74
|
+
// overrides token-sniffing. Line-anchored (`m`) so a mid-line prose mention is
|
|
75
|
+
// not treated as the metadata line; word-boundary on the value so `nope`/`not`
|
|
76
|
+
// do not match `no`.
|
|
77
|
+
const hintMatch = normalised.match(/^\s*\*\*UI hint\*\*\s*:\s*(yes|no)\b/im);
|
|
78
|
+
const hint = hintMatch ? hintMatch[1].toLowerCase() : null;
|
|
79
|
+
// Strip ANY `**UI hint**:` line before token-sniffing so a hint without a
|
|
80
|
+
// recognised yes/no (or one we did not short-circuit on) cannot false-positive
|
|
81
|
+
// on the bare `UI` token.
|
|
82
|
+
const sniffable = normalised
|
|
83
|
+
.split('\n')
|
|
84
|
+
.filter((line) => !/^\s*\*\*UI hint\*\*\s*:/i.test(line))
|
|
85
|
+
.join('\n');
|
|
70
86
|
const found = new Set();
|
|
71
|
-
for (const line of
|
|
87
|
+
for (const line of sniffable.split('\n')) {
|
|
72
88
|
// Reset lastIndex before each line so the global pattern restarts from 0.
|
|
73
89
|
UI_GATE_PATTERN_GLOBAL.lastIndex = 0;
|
|
74
90
|
for (const m of line.matchAll(UI_GATE_PATTERN_GLOBAL)) {
|
|
75
91
|
found.add(m[2].toLowerCase());
|
|
76
92
|
}
|
|
77
93
|
}
|
|
94
|
+
if (hint === 'no') {
|
|
95
|
+
return { hasUI: false, tokens: [] };
|
|
96
|
+
}
|
|
97
|
+
if (hint === 'yes') {
|
|
98
|
+
return { hasUI: true, tokens: [...found] };
|
|
99
|
+
}
|
|
78
100
|
return { hasUI: found.size > 0, tokens: [...found] };
|
|
79
101
|
}
|
|
80
102
|
// ── CLI entry point ─────────────────────────────────────────────────────────
|
|
@@ -13,6 +13,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
15
15
|
const validate_cjs_1 = require("./validate.cjs");
|
|
16
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
16
17
|
const validate_cjs_2 = require("./validate.cjs");
|
|
17
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
18
19
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
@@ -1049,7 +1050,7 @@ function cmdValidateConsistency(cwd, raw) {
|
|
|
1049
1050
|
.sort();
|
|
1050
1051
|
for (const dir of dirs) {
|
|
1051
1052
|
const phasePath = node_path_1.default.join(phaseRoot, dir);
|
|
1052
|
-
const phaseLabel = node_path_1.default.relative(planBase, phasePath)
|
|
1053
|
+
const phaseLabel = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.relative(planBase, phasePath));
|
|
1053
1054
|
const phaseFiles = node_fs_1.default.readdirSync(phasePath);
|
|
1054
1055
|
const plans = phaseFiles.filter((f) => f.endsWith('-PLAN.md')).sort();
|
|
1055
1056
|
const planNums = plans
|
|
@@ -1591,7 +1592,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1591
1592
|
stateContent += `**Current phase:** (determining...)\n`;
|
|
1592
1593
|
stateContent += `**Status:** Resuming\n\n`;
|
|
1593
1594
|
stateContent += `## Session Log\n\n`;
|
|
1594
|
-
stateContent += `- ${
|
|
1595
|
+
stateContent += `- ${clock_cjs_1.realClock.localToday()}: STATE.md regenerated by ${slash('health')} --repair\n`;
|
|
1595
1596
|
writeStateMd(statePath, stateContent, cwd);
|
|
1596
1597
|
repairActions.push({ action: repair, success: true, path: 'STATE.md' });
|
|
1597
1598
|
break;
|
|
@@ -1647,7 +1648,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1647
1648
|
case 'backfillMilestones': {
|
|
1648
1649
|
if (!options['backfill'] && !options['repair'])
|
|
1649
1650
|
break;
|
|
1650
|
-
const today =
|
|
1651
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
1651
1652
|
let backfilled = 0;
|
|
1652
1653
|
for (const ver of missingFromRegistry) {
|
|
1653
1654
|
try {
|
|
@@ -17,6 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
};
|
|
18
18
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
20
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
22
|
const io = require("./io.cjs");
|
|
22
23
|
const { output, error } = io;
|
|
@@ -154,7 +155,7 @@ function cmdWorkstreamCreate(cwd, name, options, raw) {
|
|
|
154
155
|
}
|
|
155
156
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(wsDir);
|
|
156
157
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(node_path_1.default.join(wsDir, 'phases'));
|
|
157
|
-
const today =
|
|
158
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
158
159
|
const stateContent = [
|
|
159
160
|
'---',
|
|
160
161
|
`workstream: ${slug}`,
|
|
@@ -264,7 +265,7 @@ function cmdWorkstreamComplete(cwd, name, options, raw) {
|
|
|
264
265
|
if (active === name)
|
|
265
266
|
setActiveWorkstream(cwd, null);
|
|
266
267
|
const archiveDir = node_path_1.default.join(root, 'milestones');
|
|
267
|
-
const today =
|
|
268
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
268
269
|
let archivePath = node_path_1.default.join(archiveDir, `ws-${name}-${today}`);
|
|
269
270
|
let suffix = 1;
|
|
270
271
|
while (node_fs_1.default.existsSync(archivePath)) {
|
|
@@ -440,7 +440,7 @@ function rescueSummaryArtifacts(worktreePath, repoRoot, deps) {
|
|
|
440
440
|
// relPath is the path relative to the worktree root (e.g. ".planning/q1-SUMMARY.md")
|
|
441
441
|
// Normalize to forward slashes so the Set comparison against `git status --porcelain`
|
|
442
442
|
// output works on Windows too (git always emits forward slashes in porcelain output).
|
|
443
|
-
const relPath = absPath.slice(worktreePath.length).replace(/^[/\\]/, '')
|
|
443
|
+
const relPath = (0, shell_command_projection_cjs_1.posixNormalize)(absPath.slice(worktreePath.length).replace(/^[/\\]/, ''));
|
|
444
444
|
// #706: skip rescue when the SUMMARY is already committed on the branch.
|
|
445
445
|
// Use `git cat-file -e HEAD:<relPath>` (not `ls-files --error-unmatch`) so
|
|
446
446
|
// the check is against the committed tree, not the index. ls-files also
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Write-Set — shared fail-loud parse `Result` and per-surface write-set
|
|
4
|
+
* contracts (ADR-2143, epic #2143). Pure, Node built-ins only, no I/O.
|
|
5
|
+
* Compiled by tsc to gsd-core/bin/lib/write-set.cjs.
|
|
6
|
+
*
|
|
7
|
+
* ADR-2143 §5 (fail-loud parsing, no null-swallow): seam parse operations
|
|
8
|
+
* and document-model accessors return a typed `Result<T>` — never a bare
|
|
9
|
+
* `null` a caller can mistake for "empty but fine." This is the same
|
|
10
|
+
* `{ ok: true; value: T } | { ok: false; reason: string }` shape
|
|
11
|
+
* `markdown-table.cts` already defined for `parseMarkdownTable` /
|
|
12
|
+
* `appendQuickTaskRow`; this module is now the single source of truth for
|
|
13
|
+
* it and `markdown-table.cjs` re-exports the type so existing importers of
|
|
14
|
+
* `Result` from that module keep working unchanged.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: deliberately distinct from command-routing-hub's dispatch `Result`
|
|
17
|
+
* (`{ok,data}|{ok:false,kind}`) — the two never mix (different modules,
|
|
18
|
+
* different shapes, different purposes).
|
|
19
|
+
*
|
|
20
|
+
* ADR-2143 §6 (write-set results for multi-surface commands, no
|
|
21
|
+
* OR-into-one-flag): a command that mutates more than one surface returns
|
|
22
|
+
* an explicit per-surface write-set — `{ surface, applied }` outcomes — and
|
|
23
|
+
* its top-level "did this fully succeed" signal is true only if EVERY
|
|
24
|
+
* surface in the set applied. ORing independent surfaces into a single
|
|
25
|
+
* boolean is the direct anti-pattern that let a checkbox-only partial
|
|
26
|
+
* write (#2140) report full success.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.writeSetComplete = writeSetComplete;
|
|
30
|
+
/**
|
|
31
|
+
* True only if the write-set is non-empty AND every surface in it applied.
|
|
32
|
+
* An empty write-set is never "complete" — there is nothing to be complete
|
|
33
|
+
* about, so treating it as vacuously true would let a no-op masquerade as
|
|
34
|
+
* a full success (the same OR-into-one-flag class ADR-2143 §6 prohibits).
|
|
35
|
+
*/
|
|
36
|
+
function writeSetComplete(ws) {
|
|
37
|
+
return ws.length > 0 && ws.every((o) => o.applied);
|
|
38
|
+
}
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"haiku": { "model": "anthropic/claude-haiku-4-5" }
|
|
39
39
|
},
|
|
40
40
|
"kilo": {
|
|
41
|
-
"opus":
|
|
42
|
-
"sonnet":
|
|
43
|
-
"haiku":
|
|
41
|
+
"opus": { "model": "anthropic/claude-opus-4-8" },
|
|
42
|
+
"sonnet": { "model": "anthropic/claude-sonnet-5" },
|
|
43
|
+
"haiku": { "model": "anthropic/claude-haiku-4-5" }
|
|
44
44
|
},
|
|
45
45
|
"cline": {
|
|
46
46
|
"opus": null,
|
|
@@ -86,6 +86,11 @@
|
|
|
86
86
|
"opus": null,
|
|
87
87
|
"sonnet": null,
|
|
88
88
|
"haiku": null
|
|
89
|
+
},
|
|
90
|
+
"pi": {
|
|
91
|
+
"opus": { "model": "claude-opus-4-8" },
|
|
92
|
+
"sonnet": { "model": "claude-sonnet-5" },
|
|
93
|
+
"haiku": { "model": "claude-haiku-4-5" }
|
|
89
94
|
}
|
|
90
95
|
},
|
|
91
96
|
"providerPresets": {
|
|
@@ -9,6 +9,18 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
|
|
|
9
9
|
3. **User only does what requires human judgment** - Visual checks, UX evaluation, "does this feel right?"
|
|
10
10
|
4. **Secrets come from user, automation comes from Claude** - Ask for API keys, then Claude uses them via CLI
|
|
11
11
|
5. **Auto-mode bypasses verification/decision checkpoints** — When `workflow._auto_chain_active` or `workflow.auto_advance` is true in config: human-verify auto-approves, decision auto-selects first option, human-action still stops (auth gates cannot be automated)
|
|
12
|
+
6. **`gate="blocking-human"` is never auto-approved** — a checkpoint carrying this gate stops for a human in *every* mode, including auto-mode, regardless of its type. Rule 5 does not apply to it.
|
|
13
|
+
|
|
14
|
+
**The `gate` attribute:**
|
|
15
|
+
|
|
16
|
+
| Value | Auto-mode behavior | Use for |
|
|
17
|
+
|-------|--------------------|---------|
|
|
18
|
+
| `gate="blocking"` | Bypassed per rule 5 (human-verify auto-approves, decision auto-selects) | The default. Post-hoc verification and implementation choices that are safe to take the recommended path on when unattended. |
|
|
19
|
+
| `gate="blocking-human"` | **Never bypassed.** Stops for a human in auto-mode too. | Irreversible or trust-establishing steps a human must actually see: package-legitimacy verification before install, and any decision whose default answer would be wrong to assume. |
|
|
20
|
+
|
|
21
|
+
Reach for `gate="blocking-human"` whenever auto-approving the checkpoint would defeat its purpose. If the checkpoint exists because a human must *decide* something, `blocking` is the wrong gate — auto-mode will decide it for them.
|
|
22
|
+
|
|
23
|
+
The gate spans two layers, and both must honor it. `gsd-executor` refuses to auto-approve a `gate="blocking-human"` checkpoint and escalates it via `checkpoint_return_format` precisely so a human sees it; `execute-phase`'s `checkpoint_handling` step then decides what the user is actually shown. An orchestrator that dispatches on checkpoint *type* alone would auto-approve the very checkpoint the executor just refused to auto-approve, nullifying that refusal one layer up and letting an unattended `--auto` / `--chain` run install a package no human ever vetted.
|
|
12
24
|
</overview>
|
|
13
25
|
|
|
14
26
|
<checkpoint_types>
|