* chore(#2143): markdown table model + schema registry + fail-loud pilot — Phase 1 Phase 1 of epic #2143 (ADR-2143): consolidate markdown table parsing onto a canonical seam and migrate the pilot reader. - Add src/markdown-table.cts: parseMarkdownTable (GFM tables -> typed {columns, rows} addressed by column NAME; ragged rows are typed parse errors, not silent), a single-source TABLE_SCHEMAS registry (RoadmapProgress / RequirementsTraceability / QuickTasks / Security, with variants under one id), matchTableSchema, and findTableBySchema. Result<T> is scoped to this seam (distinct from the dispatch Result). - Migrate deriveProgressFromRoadmap (src/phase-lifecycle.cts) off the position-anchored regex to name-based resolution via the seam — fixes #2137 (the 5-column milestone-grouped Progress table previously returned all-null). - Add a schema-backed `gsd-tools quick-tasks-append` subcommand and route fast.md's log_to_state through it, retiring the inline `awk NF-2` column arithmetic — fixes #2133 (addresses #2012, #2119). Cell values are escaped (| and newlines) and the STATE.md read-modify-write is atomic under readModifyWriteStateMd (lost-update race, cf. #500/#905/#1230). - Writer/reader/template parity test guards TABLE_SCHEMAS against drift (ADR-2143 §3 Generative-Fix-Divergence). Registration: .gitignore, eslint.config.mjs, docs/INVENTORY.md + INVENTORY-MANIFEST.json, CONTEXT.md glossary, docs/CLI-TOOLS.md. Behaviour-preserving for the canonical 4-column Progress table; the named bugs are driven fail-first. Extend-never-mutate (ADR-2143 §2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#2242): backfill changeset PR number (#2248) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2242): escape backslash before pipe in markdown-table cell escaping CodeQL js/incomplete-sanitization (high): escapeCell escaped | -> \| but not the backslash itself. Now escapes \ -> \\ before | -> \|, and splitTableRow unescapes both \\ -> \ and \| -> | symmetrically so cell values (incl. literal backslashes) round-trip exactly. Added backslash round-trip tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#2242): read ROADMAP Progress table by column name — supersede #2168 ad-hoc scan Rebase reconciliation with #2168 (the tactical #2137 fix that marked itself "pending #2143"). deriveProgressFromRoadmap now resolves the Progress table via a new seam helper findTableWithColumns (first table whose header is a superset of Phase/Plans Complete/Status/Completed, any order, extra columns ignored) and reads cells by NAME — order/injection-invariant per ADR-2143 §3 — instead of the exact TABLE_SCHEMAS match. This satisfies #2168's column-invariance property test while staying seam-based and preserving its `## Progress` scoping (#2012/#1445). Ragged Progress tables now resolve to null (ADR-2143 fail-loud); updated the stale state.test.cjs assertion that predated the Phase-1 migration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,12 @@
|
||||
* list-todos [area] Count and enumerate pending todos
|
||||
* list-seeds [status] List captured seeds (optional status filter)
|
||||
* verify-path-exists <path> Check file/directory existence
|
||||
* quick-tasks-append --task <text> Append a row to STATE.md's "Quick Tasks
|
||||
* Completed" table (schema-backed via
|
||||
* markdown-table.cjs; #2133/ADR-2143).
|
||||
* Fails loud (non-zero exit) on a missing
|
||||
* or unrecognized table instead of the old
|
||||
* awk NF-2 silent-skip guess.
|
||||
* config-ensure-section Initialize .planning/config.json
|
||||
* history-digest Aggregate all SUMMARY.md data
|
||||
* summary-extract <path> [--fields] Extract structured data from SUMMARY.md
|
||||
@@ -693,7 +699,7 @@ async function main() {
|
||||
'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
|
||||
'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
|
||||
'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
|
||||
'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
||||
'profile-sample, progress, project-instruction-file, prompt-budget, quick-tasks-append, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, smart-entry, state, ' +
|
||||
'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
|
||||
'Global flags:\n' +
|
||||
' --raw Emit raw output without post-processing\n' +
|
||||
@@ -1213,6 +1219,59 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'quick-tasks-append': {
|
||||
// #2133 / ADR-2143 §3,§7: schema-backed replacement for fast.md's inline
|
||||
// `awk NF-2` Quick Tasks column arithmetic. Row construction is delegated
|
||||
// to the pure appendQuickTaskRow (markdown-table.cjs); this case only
|
||||
// handles the I/O (read STATE.md, resolve date/commit, write STATE.md).
|
||||
const qtaArgs = args.slice(1);
|
||||
const qtaTask = parseNamedArgs(qtaArgs, ['task']).task || args[1];
|
||||
if (!qtaTask) {
|
||||
error('quick-tasks-append requires --task <description> (or a positional description)', ERROR_REASON.USAGE);
|
||||
}
|
||||
|
||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||
if (!fs.existsSync(statePath)) {
|
||||
error(`quick-tasks-append: STATE.md not found at ${statePath}`, ERROR_REASON.USAGE);
|
||||
}
|
||||
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
const { execGit } = require('./lib/shell-command-projection.cjs');
|
||||
const hashResult = execGit(['rev-parse', '--short', 'HEAD'], { cwd });
|
||||
const commit = hashResult.exitCode === 0 && hashResult.stdout ? hashResult.stdout : '—';
|
||||
|
||||
const { appendQuickTaskRow } = require('./lib/markdown-table.cjs');
|
||||
|
||||
// #2242 review fix: route the read -> mutate -> write cycle through
|
||||
// state.readModifyWriteStateMd (lib/state.cjs) instead of a raw
|
||||
// fs.readFileSync + fs.writeFileSync pair, so the whole read-modify-write
|
||||
// is atomic under STATE.md's lockfile — closing the lost-update race a
|
||||
// raw read/write pair left open (cf. #500/#905/#1230). This mirrors the
|
||||
// pattern every other STATE.md-mutating case in state.cts uses (e.g.
|
||||
// cmdStateAddBlocker, cmdStateAddDecision): a mutable outer variable
|
||||
// captures the pure helper's side output, and a fail-loud reason throws
|
||||
// ExitError from INSIDE the transform (readModifyWriteStateMd's finally
|
||||
// still releases the lock before the throw propagates; the transform
|
||||
// throws before returning new content, so nothing is ever written).
|
||||
let mutation;
|
||||
state.readModifyWriteStateMd(statePath, (content) => {
|
||||
const result = appendQuickTaskRow(content, { description: qtaTask, date, commit });
|
||||
if (!result.ok) {
|
||||
// Mirrors fast.md's old "skip with a brief log" behaviour (#2133): this
|
||||
// is an expected, recoverable condition (no table / unrecognized
|
||||
// schema), not a hard crash. ExitError sets a non-zero exit code (so
|
||||
// fast.md's `|| echo ...` fallback fires) without calling
|
||||
// process.exit() directly — stdout stays flushed and untouched.
|
||||
throw new ExitError(1, `⚠ quick-tasks-append: ${result.reason}`);
|
||||
}
|
||||
mutation = result.value;
|
||||
return result.value.content;
|
||||
}, cwd);
|
||||
|
||||
output({ ok: true, row: mutation.row, variant: mutation.variant }, raw, mutation.row);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'config-ensure-section': {
|
||||
// Phase 6 (#3575): dispatch via SDK executeForCjs. The catalog rebinds
|
||||
// 'config-ensure-section' to configNewProject in
|
||||
|
||||
@@ -64,40 +64,18 @@ Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:`
|
||||
|
||||
<step name="log_to_state">
|
||||
If `.planning/STATE.md` exists and has a "Quick Tasks Completed" table, append a row
|
||||
that matches the existing table's schema. If no table exists, skip silently.
|
||||
If the table's schema is unrecognized, skip with a brief log rather than append a
|
||||
malformed row.
|
||||
that matches the existing table's schema via the schema-backed `gsd-tools
|
||||
quick-tasks-append` helper (`markdown-table.cjs`'s `appendQuickTaskRow`; #2133,
|
||||
ADR-2143 §3/§7). If no table exists, skip silently. If the table's schema is
|
||||
unrecognized, the helper fails loud (non-zero exit) instead of silently guessing
|
||||
a column count — this replaces the prior inline `awk NF-2` arithmetic that was
|
||||
the root cause of #2133.
|
||||
|
||||
```bash
|
||||
_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
|
||||
# Detect whether STATE.md has a Quick Tasks Completed table
|
||||
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
||||
# Read the table header line to determine the column schema.
|
||||
# quick.md Step 7b writes two shapes:
|
||||
# 5-column (non-validate): | # | Description | Date | Commit | Directory |
|
||||
# 6-column (validate): | # | Description | Date | Commit | Status | Directory |
|
||||
HEADER_LINE=$(grep -A2 "Quick Tasks Completed" .planning/STATE.md 2>/dev/null | grep "^|" | head -1)
|
||||
# Count REAL columns: a markdown header has a leading and a trailing pipe, so
|
||||
# awk's NF counts (real columns + 2). NF-2 yields the real column count.
|
||||
# (NF-1 was the off-by-one root cause of #2133: it returned the pipe count,
|
||||
# making the `-eq 5` test unsatisfiable for the very header quick.md writes.)
|
||||
COL_COUNT=$(echo "$HEADER_LINE" | awk -F'|' '{print NF-2}')
|
||||
|
||||
# Next row number + latest commit hash are schema-independent.
|
||||
NEXT_NUM=$(awk '/Quick Tasks Completed/{found=1} found && /^\|/ && !/^[|][-: |]*[|]$/ && !/Description/{count++} END{print count+1}' .planning/STATE.md 2>/dev/null || echo "1")
|
||||
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "—")
|
||||
|
||||
# Select the appended row's template by the detected column count so its cell
|
||||
# count always matches the header (prevents the malformed-row symptom of #27).
|
||||
if [ "$COL_COUNT" -eq 5 ] && echo "$HEADER_LINE" | grep -qi "Description" && echo "$HEADER_LINE" | grep -qi "Commit" && echo "$HEADER_LINE" | grep -qi "Directory"; then
|
||||
# 5-column schema from quick.md Step 7b (non-validate).
|
||||
echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — |" >> .planning/STATE.md
|
||||
elif [ "$COL_COUNT" -eq 6 ] && echo "$HEADER_LINE" | grep -qi "Status" && echo "$HEADER_LINE" | grep -qi "Directory"; then
|
||||
# 6-column schema from quick.md Step 7b (validate, with Status).
|
||||
echo "| $NEXT_NUM | $TASK | $(date +%Y-%m-%d) | $COMMIT_HASH | — | — |" >> .planning/STATE.md
|
||||
else
|
||||
# Unrecognized table schema — skip to avoid appending a malformed row (#27).
|
||||
echo "⚠ fast.md log_to_state: Quick Tasks Completed table has unrecognized schema (${COL_COUNT} columns); skipping STATE.md update."
|
||||
fi
|
||||
gsd_run quick-tasks-append --task "$TASK" || echo "⚠ fast.md log_to_state: could not append Quick Tasks row (see message above); continuing."
|
||||
fi
|
||||
```
|
||||
</step>
|
||||
|
||||
@@ -971,6 +971,8 @@ Use `date` from init:
|
||||
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
|
||||
```
|
||||
|
||||
For a schema-safe append outside this workflow (e.g. from fast.md), `gsd-tools quick-tasks-append --task <text>` performs the equivalent write via the shared, schema-backed `appendQuickTaskRow` helper (#2133, ADR-2143 §3/§7).
|
||||
|
||||
**7d. Update "Last activity" line:**
|
||||
|
||||
Use `date` from init:
|
||||
|
||||
Reference in New Issue
Block a user