@jenga-ai/agent 1.2.4 → 2.0.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/README.md +97 -91
- package/agents/developer.md +26 -7
- package/agents/scrum-master.md +57 -22
- package/agents/tester.md +68 -4
- package/hooks/on_session_end.sh +40 -1
- package/lib/generate-agent-context.js +18 -1
- package/lib/generate-copilot-instructions.js +18 -1
- package/lib/generate-skill-allow-list.js +191 -0
- package/lib/skill-allow-list.json +43 -0
- package/package.json +35 -20
- package/scripts/apply-j-prefix.sh +230 -0
- package/scripts/consume-context-digest.sh +103 -0
- package/scripts/postinstall.js +25 -0
- package/scripts/sweep-stale-context-digests.sh +132 -0
- package/scripts/validate-board.sh +5 -0
- package/scripts/write-context-digest.sh +230 -0
- package/skills/brainstorm/SKILL.md +1 -1
- package/skills/btw/SKILL.md +1 -1
- package/skills/clearify/SKILL.md +1 -1
- package/skills/close-story/SKILL.md +78 -6
- package/skills/close-story/scripts/check-privatized.sh +345 -0
- package/skills/commit/SKILL.md +12 -2
- package/skills/continue/SKILL.md +1 -1
- package/skills/deep-dive/SKILL.md +1 -1
- package/skills/dev-done/SKILL.md +46 -0
- package/skills/dev-done/scripts/classify-commit-outcome.sh +114 -0
- package/skills/distribute/SKILL.md +1 -1
- package/skills/do/SKILL.md +100 -10
- package/skills/doc/README.md +155 -0
- package/skills/doc/SKILL.md +43 -13
- package/skills/doc/authoring-notes.md +72 -0
- package/skills/doc/scripts/resolve_last_update.py +149 -0
- package/skills/doc-sync/SKILL.md +1 -1
- package/skills/dooo/SKILL.md +1 -1
- package/skills/error/SKILL.md +1 -1
- package/skills/evaluate/SKILL.md +1 -1
- package/skills/examplify/SKILL.md +1 -1
- package/skills/help/SKILL.md +1 -1
- package/skills/idea/SKILL.md +1 -1
- package/skills/improve/SKILL.md +1 -1
- package/skills/init/SKILL.md +8 -7
- package/skills/init/assets/scope-thresholds_template.json +7 -0
- package/skills/init/scripts/init.sh +6 -0
- package/skills/j-init/SKILL.md +168 -0
- package/skills/j-init/assets/.gitignore_template +15 -0
- package/skills/j-init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/j-init/assets/directory_structure.txt +14 -0
- package/skills/j-init/assets/scope-thresholds_template.json +7 -0
- package/skills/j-init/assets/strategy_stub_template.md +38 -0
- package/skills/j-init/assets/test-config_template.json +4 -0
- package/skills/j-init/assets/workflow_template.json +30 -0
- package/skills/j-init/scripts/apply-project-visibility.sh +176 -0
- package/skills/j-init/scripts/detect-existing-codebase.sh +166 -0
- package/skills/j-init/scripts/init.sh +116 -0
- package/skills/jbp/SKILL.md +1 -1
- package/skills/jenga/SKILL.md +1 -1
- package/skills/jenga/scripts/render-confirmation.sh +55 -18
- package/skills/jenga-permission-level/SKILL.md +1 -1
- package/skills/lgtm/SKILL.md +1 -1
- package/skills/pi-plan/SKILL.md +1 -1
- package/skills/proceed/SKILL.md +1 -1
- package/skills/publish/SKILL.md +67 -1
- package/skills/publish/adapters/npm-ci.md +60 -4
- package/skills/publish/adapters/npm.md +18 -0
- package/skills/publish/assets/ci-contract.md +27 -0
- package/skills/publish/assets/publish.example.json +27 -0
- package/skills/publish/schemas/publish.schema.json +20 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +50 -1
- package/skills/publish/scripts/npm_stage_inspect.sh +829 -0
- package/skills/publish/scripts/npm_stage_pipeline.sh +427 -0
- package/skills/publish/scripts/publish_common.sh +16 -0
- package/skills/publish/scripts/show_history.sh +12 -5
- package/skills/publish/scripts/validate_npm_stage_env.sh +184 -0
- package/skills/publish/scripts/write_ledger_entry.sh +92 -2
- package/skills/reconcile/SKILL.md +122 -12
- package/skills/reconcile/assets/report_format.md +17 -0
- package/skills/reconcile/scripts/resolve-reconcile-scope.sh +489 -0
- package/skills/reconcile-origin/SKILL.md +1 -1
- package/skills/redo/SKILL.md +1 -1
- package/skills/skillify/SKILL.md +1 -1
- package/skills/spinoff/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/todo/SKILL.md +40 -3
- package/skills/todo/scripts/add_trivial_task.sh +216 -0
- package/skills/todo/scripts/update_story_tasks.py +87 -0
- package/skills/uncharted/SKILL.md +201 -22
- package/skills/uncharted/scripts/directory-triage.sh +342 -0
- package/skills/uncharted/scripts/elicitation-state.sh +457 -0
- package/skills/wtf/SKILL.md +1 -1
- package/templates/SCRUM_BOARD_SCHEMA.md +90 -2
- package/templates/agent-context.md.tpl +47 -12
- package/templates/copilot-instructions.md.tpl +36 -9
- package/mcp/router/README.md +0 -19
- package/mcp/router/embedder.js +0 -23
- package/mcp/router/index.js +0 -204
- package/mcp/router/matcher.js +0 -87
- package/mcp/router/package-lock.json +0 -1048
- package/mcp/router/package.json +0 -11
- package/mcp/router/skill-index.js +0 -104
- package/skills/route/SKILL.md +0 -180
package/skills/do/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: do
|
|
2
|
+
name: j:do
|
|
3
3
|
description: Execute tasks from the scrum board. Reads from project/todo.md, resolves each entry to its full scrum board context, and drives the developer agent through implementation with the correct sender object and communication contract. Loops until all selected tasks are done or the user exits.
|
|
4
4
|
keywords:
|
|
5
5
|
- do
|
|
@@ -16,6 +16,14 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
# Do — Execute Scrum Board Tasks
|
|
18
18
|
|
|
19
|
+
## `--trivial` Flag
|
|
20
|
+
|
|
21
|
+
**Syntax:** `/do <id> --trivial` — a dispatch-time override, distinct from `/todo --trivial` (a creation-time flag documented in `skills/todo/SKILL.md`). Where `/todo --trivial` writes a brand-new task straight to `execution_scope: inline`, `/do <id> --trivial` overrides an **already-existing** task's `execution_scope` — whatever it currently is, including absent (legacy tasks with no execution-scope fields at all) — to `inline` at the moment it's dispatched. See `### 4.1.5. \`--trivial\` Dispatch-Time Override` below for the full mechanics.
|
|
22
|
+
|
|
23
|
+
**No softer fallback tier — hard fallback to the full pipeline instead.** Unlike `light` scope (which sits between `inline` and `task`), `--trivial` always forces `inline` directly, with no intermediate tier to fall back to first. To compensate, a `--trivial`-forced run that fails `scripts/smoke-harness.sh` or shows detected scope creep mid-run automatically re-routes to the full `task` pipeline (worktree + developer + tester) via the same `#### Fallback to Full Task-Scope Pipeline` procedure the `light`-tier fallback uses — see `### 4.2`'s failure-handling branches and the shared Fallback subsection under `### 4.3`.
|
|
24
|
+
|
|
25
|
+
**Human-only, same as `/todo --trivial`.** `--trivial` is invoked by a human typing `/do <id> --trivial` — it is never applied autonomously by `/jenga` or the scrum-master's own breakdown/dispatch passes.
|
|
26
|
+
|
|
19
27
|
## Instructions
|
|
20
28
|
|
|
21
29
|
### 0. Load threshold config
|
|
@@ -318,9 +326,40 @@ Before invoking the developer, check whether this task was manually scoped by a
|
|
|
318
326
|
```
|
|
319
327
|
Then proceed to step 4.2.
|
|
320
328
|
|
|
329
|
+
### 4.1.5. `--trivial` Dispatch-Time Override
|
|
330
|
+
|
|
331
|
+
After override validation (step 4.1) and before branching on `execution_scope` in step 4.2, check whether this invocation was `/do <id> --trivial`.
|
|
332
|
+
|
|
333
|
+
1. **Detect the flag.** If the task was invoked as `/do <id>` with no `--trivial` flag, skip this entire section and proceed directly to `### 4.2`.
|
|
334
|
+
|
|
335
|
+
2. **If `--trivial` is present**, read the task's current `execution_scope` from frontmatter. Treat an absent value as `task`, per the epic's backward-compatibility rule (a task that omits `execution_scope` is treated as `execution_scope: task`). Call this the **prior tier** — this covers both tasks nobody flagged as trivial at creation (an already-assigned `story`/`task`/`light` tier) and legacy tasks with no `execution_scope` set at all.
|
|
336
|
+
|
|
337
|
+
3. **Overwrite `execution_scope` to `inline`** in the task's frontmatter, unconditionally — `--trivial` always forces `inline`, never a softer "lightest safe tier."
|
|
338
|
+
|
|
339
|
+
4. **Record the override for audit**, reusing the existing `jenga_assigned` / `override_justification` pairing already defined in `templates/SCRUM_BOARD_SCHEMA.md` for exactly this situation ("scope overridden by a human"), rather than inventing a new field:
|
|
340
|
+
- Set `jenga_assigned: false` (if not already `false`).
|
|
341
|
+
- Set (or append to, if already present) `override_justification`:
|
|
342
|
+
```
|
|
343
|
+
override_justification: "/do --trivial dispatch-time override on <date>: execution_scope forced from '<prior_tier>' to 'inline' by human operator."
|
|
344
|
+
```
|
|
345
|
+
- Also set (or append to) `scope_rationale`, mirroring the phrasing convention `skills/todo/scripts/add_trivial_task.sh` already uses for the creation-time flag, so both audit fields agree on the prior tier:
|
|
346
|
+
```
|
|
347
|
+
scope_rationale: "forced inline via /do --trivial (dispatch-time override); prior execution_scope was '<prior_tier>'"
|
|
348
|
+
```
|
|
349
|
+
- Emit a non-fatal log line (styled like 4.2's `AUTO-CORRECTION` message):
|
|
350
|
+
```
|
|
351
|
+
TRIVIAL OVERRIDE [<task_id>]: execution_scope forced from "<prior_tier>" to "inline" via --trivial dispatch-time override.
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
5. **Set an in-session marker** (this dispatch is a `--trivial`-forced run) — this does not need to be persisted to frontmatter; it only needs to survive for the remainder of this `/do` invocation. This marker is distinct from an organically-assigned `execution_scope: inline` task (one the scrum-master or `/jenga` assigned `inline` to directly, with no `--trivial` involved) because its failure handling differs — see `### 4.2`'s failure-handling steps below. Do not confuse a `--trivial`-forced run with an organic `inline` task when applying those steps.
|
|
355
|
+
|
|
356
|
+
6. **Proceed to `### 4.2. Inline Execution Path`** with `execution_scope` now `inline`. Everything else about inline execution (implementation, smoke test invocation, commit convention) is identical between an organic `inline` task and a `--trivial`-forced one — only the two failure-handling branches in `### 4.2` differ, per the marker set in step 5 above.
|
|
357
|
+
|
|
358
|
+
**Precedence with the locked-task dispatch guard.** If `crucial_level: locked` (checked by `### 4.2`'s locked-task dispatch guard, which always runs and always wins), `--trivial` is redundant but harmless — the task was already going to be forced `inline`. The locked-task guard's own audit fields take precedence for that correction; do not double-write conflicting `override_justification` text. A `locked` task's fallback behavior remains "halt and report," never the `--trivial` fallback below, regardless of whether `--trivial` was also passed.
|
|
359
|
+
|
|
321
360
|
### 4.2. Inline Execution Path (execution_scope: inline)
|
|
322
361
|
|
|
323
|
-
After resolving the task context (step 4)
|
|
362
|
+
After resolving the task context (step 4), passing override validation (step 4.1), and applying the `--trivial` dispatch-time override if present (step 4.1.5), read `execution_scope` from the task frontmatter.
|
|
324
363
|
|
|
325
364
|
**Locked-task dispatch guard (defense-in-depth).** Before branching on `execution_scope` below, read `crucial_level` from the task frontmatter (per `templates/SCRUM_BOARD_SCHEMA.md`'s Crucial Flag Fields). If `crucial_level: locked`:
|
|
326
365
|
|
|
@@ -349,12 +388,14 @@ After resolving the task context (step 4) and passing override validation (step
|
|
|
349
388
|
WARNING [<task_id>]: scripts/smoke-harness.sh not found. Smoke test skipped (stub pass).
|
|
350
389
|
```
|
|
351
390
|
4. **If the smoke test exits non-zero**:
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
391
|
+
- **If this is a `--trivial`-forced run** (marker set in step 4.1.5 — and `crucial_level` is not `locked`, which never falls back, per 4.1.5's precedence note): do NOT write `status: Failed`. `--trivial` always forces `inline` with no softer "lightest safe tier" to fall back to first, so a smoke-harness failure here goes straight to the shared `#### Fallback to Full Task-Scope Pipeline` procedure below (origin: `trivial`). Do not proceed with the remaining inline steps below — the Fallback procedure takes over from here.
|
|
392
|
+
- **Otherwise** (an organically-assigned `inline` task, `--trivial` not involved): behavior is unchanged from before —
|
|
393
|
+
- Write `status: Failed` to the task's frontmatter.
|
|
394
|
+
- Emit:
|
|
395
|
+
```
|
|
396
|
+
INLINE TASK FAILED [<task_id>]: smoke test returned non-zero exit code. Task marked Failed. Halting.
|
|
397
|
+
```
|
|
398
|
+
- Do not commit. Do not proceed to the next task.
|
|
358
399
|
5. **If the smoke test passes**:
|
|
359
400
|
- Commit the changes using the standard commit convention (`task(<task_id>): <short description>`) via `/commit` in inline mode (E32_S04_T03).
|
|
360
401
|
- Run the **Intent-vs-Diff Check** (see `### 5.1. Intent-vs-Diff Check` below) for this task.
|
|
@@ -365,9 +406,58 @@ After resolving the task context (step 4) and passing override validation (step
|
|
|
365
406
|
7. `inline` tasks always have `needs_docs: false` — skip plan and summary documentation for the implemented task.
|
|
366
407
|
8. Continue to `### 6. Verify documentation`, then `### 7. After successful completion`.
|
|
367
408
|
|
|
368
|
-
If the implementation cannot be completed inline (scope is larger than anticipated
|
|
409
|
+
If the implementation cannot be completed inline (scope is larger than anticipated — detected scope creep mid-run):
|
|
410
|
+
- **If this is a `--trivial`-forced run** (and `crucial_level` is not `locked`): invoke the shared `#### Fallback to Full Task-Scope Pipeline` procedure below (origin: `trivial`) — the same procedure the smoke-harness-failure branch above uses, not a second bespoke re-route.
|
|
411
|
+
- **Otherwise** (an organically-assigned `inline` task): abort and re-route to the normal developer path (step 5), unchanged from before.
|
|
412
|
+
- **Regardless of `--trivial`**, if `crucial_level: locked`, do not re-route via either path above; re-attempt inline or halt and report, per the locked-task dispatch guard above.
|
|
369
413
|
|
|
370
|
-
**If `execution_scope` is
|
|
414
|
+
**If `execution_scope` is `light`** (and `crucial_level` is not `locked` — the locked-task dispatch guard above already ran and takes precedence over any scope check), proceed to `### 4.3. Light Execution Path` below instead of step 5.
|
|
415
|
+
|
|
416
|
+
**If `execution_scope` is not `inline` and not `light`** (or is absent / `task` / `story` / `epic`) **and `crucial_level` is not `locked`**, proceed to step 5 (invoke the developer agent) as normal.
|
|
417
|
+
|
|
418
|
+
### 4.3. Light Execution Path (execution_scope: light)
|
|
419
|
+
|
|
420
|
+
After resolving the task context (step 4), passing override validation (step 4.1), and applying the `--trivial` dispatch-time override if present (step 4.1.5 — note `--trivial` always forces `inline`, so a `light`-scoped task only reaches this section if `--trivial` was *not* passed), if `execution_scope: light` and `crucial_level` is not `locked` (per the locked-task dispatch guard in 4.2, which runs first and always wins), route the task through this path instead of the full `task`-scope pipeline in step 5.
|
|
421
|
+
|
|
422
|
+
`light` sits between `inline` and `task`: unlike `inline`, it spawns a real developer subagent (so it can handle small branching logic that inline's main-session execution isn't suited for); unlike `task`, it does not create a dedicated worktree and does not invoke the tester as a separate step.
|
|
423
|
+
|
|
424
|
+
1. **Spawn a developer subagent** (Agent tool, `subagent_type: "developer"`) with the same sender object and context payload as step 5 would use, but with an explicit instruction added to the dispatch prompt: **do not create a worktree** — implement directly against the current checkout (the session's existing working tree), not an isolated `.claude/worktrees/<slug>` copy. This is the one concrete difference from the step-5 `task` path: everything else about how the subagent implements the task (reading the task file, following acceptance criteria, following repo conventions) is unchanged.
|
|
425
|
+
|
|
426
|
+
2. **After the developer subagent reports implementation complete**, run the smoke test harness using the same invocation convention as `### 4.2. Inline Execution Path`:
|
|
427
|
+
- Run `bash scripts/smoke-harness.sh <changed_file>...`, passing the paths the subagent changed. With no arguments the harness infers them from `git diff --name-only HEAD`. It exits `0` on pass and `1` on failure.
|
|
428
|
+
- If `scripts/smoke-harness.sh` does not exist, log a warning and treat the result as a pass:
|
|
429
|
+
```
|
|
430
|
+
WARNING [<task_id>]: scripts/smoke-harness.sh not found. Smoke test skipped (stub pass).
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
3. **If the smoke test passes**:
|
|
434
|
+
- The developer subagent self-verifies the implementation against the task's acceptance criteria. No tester subagent is invoked for a `light`-scoped task — this is a deliberate, documented exception to "the tester is the sole status-writer" (`agents/tester.md`), mirroring the same exception already established for `inline` scope in step 5 of `### 4.2`. Since no tester runs, the developer/orchestrator is the one who writes the terminal status for a `light`-scoped task.
|
|
435
|
+
- Commit the changes using the standard commit convention (`task(<task_id>): <short description>`) via `/commit`.
|
|
436
|
+
- Run the **Intent-vs-Diff Check** (see `### 5.1. Intent-vs-Diff Check` below) for this task.
|
|
437
|
+
- Write `status: Passed` and `date_completed: <today>` to the task's frontmatter if self-verification passes.
|
|
438
|
+
- Remove the task from `project/todo.md`.
|
|
439
|
+
- Continue to `### 6. Verify documentation`, then `### 7. After successful completion`.
|
|
440
|
+
|
|
441
|
+
4. **If the smoke test fails (non-zero exit)**: do NOT write `status: Failed` and do NOT halt. Instead, invoke `#### Fallback to Full Task-Scope Pipeline` below (origin: `light`).
|
|
442
|
+
|
|
443
|
+
#### Fallback to Full Task-Scope Pipeline
|
|
444
|
+
|
|
445
|
+
This is a self-contained, reusable procedure with two current callers — `### 4.2`'s `--trivial`-forced inline failure branches (origin: `trivial`) and `### 4.3`'s `light`-scope smoke-harness failure (origin: `light`) — given a task that was attempted under a reduced-overhead execution scope and failed its smoke-harness check (or, for `trivial`, showed detected scope creep mid-run), do the following. The only thing that varies by caller is the notice text in step 5; steps 1–4 and 6 are identical regardless of origin.
|
|
446
|
+
|
|
447
|
+
1. **Do not mark the task `Failed`.** A smoke-harness failure (or detected scope creep) under a reduced-overhead scope means the scope was too small for the task, not that the task itself is unworkable — the correct response is to retry under full isolation, not to reject the work.
|
|
448
|
+
2. **Create a worktree** for the task, named `<E##_S##_T##-short-slug>` per standard Worktree Management conventions, if one does not already exist for this task. (A task dispatched under `light` scope, or forced `inline` via `--trivial`, never had one — both premises skip worktree creation — so this step always creates a fresh worktree in that case.)
|
|
449
|
+
3. **Spawn a developer subagent** in that worktree and have it pick up from the current state of the code (the changes already made by the reduced-overhead attempt are still present in the working tree / already committed, if any commit occurred — the subagent continues from there rather than starting over).
|
|
450
|
+
4. **Invoke the tester agent** per the normal `### 5. Invoke the developer agent` flow's contract — full sender object, commit SHAs, worktree path. The tester is responsible for the terminal status write, exactly as in the standard `task`-scope pipeline.
|
|
451
|
+
5. **Emit a clear, non-fatal fallback notice** to the user/orchestrator, using the message matching the caller's origin:
|
|
452
|
+
- origin `light`:
|
|
453
|
+
```
|
|
454
|
+
LIGHT SCOPE FALLBACK [<task_id>]: smoke test failed; re-routing to full task-scope pipeline (worktree + developer + tester).
|
|
455
|
+
```
|
|
456
|
+
- origin `trivial`:
|
|
457
|
+
```
|
|
458
|
+
TRIVIAL OVERRIDE FALLBACK [<task_id>]: smoke test failed (or scope creep detected); re-routing to full task-scope pipeline (worktree + developer + tester).
|
|
459
|
+
```
|
|
460
|
+
6. Resume normal `task`-scope processing (steps 6–8 below) once the tester returns a verdict.
|
|
371
461
|
|
|
372
462
|
### 5. Invoke the developer agent
|
|
373
463
|
Pass the following to the developer agent:
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# `/doc` Authoring and Usage Guide
|
|
2
|
+
|
|
3
|
+
`/doc` generates or regenerates a complete Markdown document for a resolved target path. The skill owns the entire target file: it resolves the documentation objective first, gathers evidence, reads any existing target for still-valid maintainer intent, then writes a full replacement document.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
### Default target
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
/doc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When no target is provided, `/doc` resolves the default target from `skills/doc/assets/path-objectives.yaml`. Today that default is `README.md`.
|
|
14
|
+
|
|
15
|
+
Expected flow:
|
|
16
|
+
1. Resolve `README.md`
|
|
17
|
+
2. Match the `project overview` objective
|
|
18
|
+
3. Gather project evidence
|
|
19
|
+
4. Regenerate the full `README.md`
|
|
20
|
+
|
|
21
|
+
### Custom target
|
|
22
|
+
|
|
23
|
+
Canonical custom-target form:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
/doc docs/API.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Accepted convenience form:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
/doc update: docs/API.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Both forms resolve `docs/API.md`, then apply the matching rule from `skills/doc/assets/path-objectives.yaml`.
|
|
36
|
+
|
|
37
|
+
## Target Resolution and Objective Rules
|
|
38
|
+
|
|
39
|
+
`skills/doc/assets/path-objectives.yaml` is the source of truth for:
|
|
40
|
+
- the default target
|
|
41
|
+
- known target paths
|
|
42
|
+
- each path's documentation objective
|
|
43
|
+
- required and optional sections for known targets
|
|
44
|
+
|
|
45
|
+
Known targets bypass ambiguity. Unknown targets must stop for clarification.
|
|
46
|
+
|
|
47
|
+
## Evidence Sources and Precedence
|
|
48
|
+
|
|
49
|
+
`/doc` builds a synthesis context before writing. When sources disagree, use this precedence order:
|
|
50
|
+
1. explicit user instruction
|
|
51
|
+
2. scrum-board context
|
|
52
|
+
3. codebase evidence
|
|
53
|
+
4. git history
|
|
54
|
+
|
|
55
|
+
The synthesis context contract currently includes:
|
|
56
|
+
- `target_path`
|
|
57
|
+
- `objective`
|
|
58
|
+
- `project_name`
|
|
59
|
+
- `project_description`
|
|
60
|
+
- `features`
|
|
61
|
+
- `getting_started`
|
|
62
|
+
- `board_items`
|
|
63
|
+
- `conflicts_resolved`
|
|
64
|
+
- `sources_used`
|
|
65
|
+
- `existing_intent`
|
|
66
|
+
|
|
67
|
+
Use stronger sources to break ties. Do not let weaker evidence overwrite explicit user direction.
|
|
68
|
+
|
|
69
|
+
## `last_update` Provenance
|
|
70
|
+
|
|
71
|
+
`last_update` is the documentation provenance field described by Epic E24's provenance work. It is intended to capture the most recent completed board item that materially updated the target document.
|
|
72
|
+
|
|
73
|
+
### Expected source of truth
|
|
74
|
+
|
|
75
|
+
The provenance lookup relies on scrum-board `docs: [...]` annotations that point at repo-relative documentation targets such as:
|
|
76
|
+
- `README.md`
|
|
77
|
+
- `docs/API.md`
|
|
78
|
+
|
|
79
|
+
Board authors should add `docs` annotations to stories or tasks whenever implementation work changes a specific document or should be reflected in that document later.
|
|
80
|
+
|
|
81
|
+
### How provenance is expected to resolve
|
|
82
|
+
|
|
83
|
+
1. Look for completed board items whose `docs` list includes the target path exactly.
|
|
84
|
+
2. Prefer the most recent qualifying item.
|
|
85
|
+
3. Use that item as the basis for the document's `last_update` value.
|
|
86
|
+
|
|
87
|
+
### Fallback behavior
|
|
88
|
+
|
|
89
|
+
If provenance cannot be resolved, `/doc` should follow the fallback defined by E24_S05:
|
|
90
|
+
- omit `last_update`, or
|
|
91
|
+
- mark it as `unknown`
|
|
92
|
+
|
|
93
|
+
Typical failure modes:
|
|
94
|
+
- the relevant board item never declared `docs: [...]`
|
|
95
|
+
- the target path in the board item does not exactly match the generated path
|
|
96
|
+
- the board item exists but is not yet in a completed/passed state
|
|
97
|
+
|
|
98
|
+
## Ambiguity Gate
|
|
99
|
+
|
|
100
|
+
If the resolved target path is not present in `skills/doc/assets/path-objectives.yaml`, `/doc` must not guess.
|
|
101
|
+
|
|
102
|
+
It should stop and ask exactly:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
What should <target_path> document? Please describe the objective.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
/doc update: docs/UNKNOWN.md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Expected behavior:
|
|
115
|
+
- do not generate a file yet
|
|
116
|
+
- do not invent a target objective
|
|
117
|
+
- ask the user what `docs/UNKNOWN.md` is meant to document
|
|
118
|
+
|
|
119
|
+
Once the user clarifies the objective, surface the resolved contract and continue.
|
|
120
|
+
|
|
121
|
+
## Target-Specific Notes
|
|
122
|
+
|
|
123
|
+
### `README.md`
|
|
124
|
+
Use `/doc` with no arguments when you want to regenerate the project overview. The generated file should center on:
|
|
125
|
+
- project description
|
|
126
|
+
- getting started steps
|
|
127
|
+
- examples only when they are strongly supported by evidence
|
|
128
|
+
|
|
129
|
+
### `docs/API.md`
|
|
130
|
+
Use `/doc docs/API.md` (or `/doc update: docs/API.md`) when you want an API reference document. The generated file should remain grounded in known interfaces, endpoints, parameters, return values, and errors.
|
|
131
|
+
|
|
132
|
+
## Tips for Board Authors
|
|
133
|
+
|
|
134
|
+
Add `docs` annotations when board work affects documentation scope or provenance. Good examples:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
docs:
|
|
138
|
+
- README.md
|
|
139
|
+
- docs/API.md
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use annotations when:
|
|
143
|
+
- a task introduces or changes user-visible behavior that belongs in README
|
|
144
|
+
- a story adds or changes an endpoint, command, interface, or workflow doc
|
|
145
|
+
- you want `/doc` provenance to trace the work back to the board reliably
|
|
146
|
+
|
|
147
|
+
Avoid annotations when the work has no documentation impact.
|
|
148
|
+
|
|
149
|
+
## Maintainer Checklist
|
|
150
|
+
|
|
151
|
+
Before relying on `/doc`, confirm that:
|
|
152
|
+
1. the target path exists in `skills/doc/assets/path-objectives.yaml`, or you are prepared to answer the ambiguity prompt
|
|
153
|
+
2. relevant board items include accurate `docs: [...]` annotations
|
|
154
|
+
3. higher-priority evidence sources are up to date
|
|
155
|
+
4. any existing target file content that should survive regeneration is genuinely still valid
|
package/skills/doc/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: doc
|
|
2
|
+
name: j:doc
|
|
3
3
|
description: Generate or update a documentation file by resolving a target path to a clear documentation objective before writing.
|
|
4
4
|
metadata:
|
|
5
5
|
prefered_agent: developer
|
|
@@ -12,6 +12,7 @@ keywords:
|
|
|
12
12
|
examples:
|
|
13
13
|
- "/doc"
|
|
14
14
|
- "/doc docs/API.md"
|
|
15
|
+
- "/doc update: docs/API.md"
|
|
15
16
|
- "generate documentation for the CLI"
|
|
16
17
|
- "update the contributing guide"
|
|
17
18
|
---
|
|
@@ -25,6 +26,7 @@ examples:
|
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
- If `target-path` is omitted, default to `README.md`.
|
|
29
|
+
- If the remainder starts with `update:`, strip that prefix, then trim again before resolving the target path.
|
|
28
30
|
- If `target-path` is provided, use it exactly as written after `/doc`.
|
|
29
31
|
- Do not guess additional arguments or rewrite the requested path.
|
|
30
32
|
|
|
@@ -32,6 +34,8 @@ examples:
|
|
|
32
34
|
|
|
33
35
|
Load `skills/doc/assets/path-objectives.yaml` before resolving the documentation objective. Treat it as the source of truth for the `default_target`, known target paths, and their structural requirements.
|
|
34
36
|
|
|
37
|
+
For extended usage guidance, provenance notes, and board-author tips, see `skills/doc/README.md`.
|
|
38
|
+
|
|
35
39
|
## Synthesis Context Contract
|
|
36
40
|
|
|
37
41
|
After target resolution, all generation must operate on a synthesis context object. E24_S03 is responsible for producing the final implementation, but this story defines the field contract that downstream generation must consume.
|
|
@@ -47,6 +51,7 @@ board_items: []
|
|
|
47
51
|
conflicts_resolved: []
|
|
48
52
|
sources_used: []
|
|
49
53
|
existing_intent: null
|
|
54
|
+
last_update: null
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
Required fields from E24_S03:
|
|
@@ -60,8 +65,9 @@ Required fields from E24_S03:
|
|
|
60
65
|
- `conflicts_resolved`
|
|
61
66
|
- `sources_used`
|
|
62
67
|
- `existing_intent`
|
|
68
|
+
- `last_update`
|
|
63
69
|
|
|
64
|
-
If the shared collector from E24_S03 is not yet merged, construct a temporary context with the same field names so later steps remain compatible.
|
|
70
|
+
If the shared collector from E24_S03 is not yet merged, construct a temporary context with the same field names so later steps remain compatible. `last_update` is resolved during the provenance step below.
|
|
65
71
|
|
|
66
72
|
## Instructions
|
|
67
73
|
|
|
@@ -70,12 +76,14 @@ If the shared collector from E24_S03 is not yet merged, construct a temporary co
|
|
|
70
76
|
1. Read `default_target` from `skills/doc/assets/path-objectives.yaml`. If it is missing, fall back to `README.md`.
|
|
71
77
|
2. Remove the `/doc` command token from the invocation.
|
|
72
78
|
3. Trim the remaining text.
|
|
73
|
-
4. If
|
|
74
|
-
5.
|
|
79
|
+
4. If the trimmed remainder starts with the exact prefix `update:`, remove that prefix and trim the remainder again.
|
|
80
|
+
5. If nothing remains, set `target_path` to `default_target`.
|
|
81
|
+
6. Otherwise, set `target_path` to the trimmed remainder.
|
|
75
82
|
|
|
76
83
|
Examples:
|
|
77
84
|
- `/doc` → `target_path = README.md`
|
|
78
85
|
- `/doc docs/API.md` → `target_path = docs/API.md`
|
|
86
|
+
- `/doc update: docs/API.md` → `target_path = docs/API.md`
|
|
79
87
|
- `/doc docs/CLI.md` → `target_path = docs/CLI.md`
|
|
80
88
|
|
|
81
89
|
### 2. Resolve the objective from the rule table
|
|
@@ -135,15 +143,37 @@ If `target_path` is not present in `skills/doc/assets/path-objectives.yaml`:
|
|
|
135
143
|
|
|
136
144
|
If the target file does not exist, keep `existing_intent = null`.
|
|
137
145
|
|
|
138
|
-
### 7.
|
|
146
|
+
### 7. Resolve `last_update` provenance before writing
|
|
147
|
+
|
|
148
|
+
1. Run `python3 skills/doc/scripts/resolve_last_update.py <target_path>` from the repository root.
|
|
149
|
+
2. The resolver must scan `project/board/epics/`, `project/board/stories/`, and `project/board/tasks/`.
|
|
150
|
+
3. Treat a board item as provenance only when all of the following are true:
|
|
151
|
+
- `status` is exactly `Done` or `Passed`
|
|
152
|
+
- `docs` is a YAML list that contains `target_path` as an exact repo-relative string match
|
|
153
|
+
- `date_completed` is present and parses as `YYYY-MM-DD`
|
|
154
|
+
4. If multiple board items match, select the most recent `date_completed` and store it in `synthesis_context.last_update`.
|
|
155
|
+
5. If no matching provenance is found, set `synthesis_context.last_update = "unknown"`. This is the required fallback because it keeps the frontmatter shape stable while making the missing provenance explicit.
|
|
156
|
+
6. Ignore board items in any other status, items missing `docs`, and items whose `docs` entry uses a non-matching path form.
|
|
157
|
+
|
|
158
|
+
### 8. Generate a complete replacement file
|
|
139
159
|
|
|
140
160
|
1. Build a **full file string** from the synthesis context and the resolved target objective.
|
|
141
|
-
2.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
2. Start the file with YAML frontmatter for provenance, even when provenance could not be resolved:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
---
|
|
165
|
+
last_update: <YYYY-MM-DD or unknown>
|
|
166
|
+
---
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
3. Use the resolved `synthesis_context.last_update` value in that frontmatter. Emit `unknown` verbatim when no completed board item provides provenance.
|
|
170
|
+
4. This fallback is mandatory: do not omit the `last_update` key when provenance is missing.
|
|
171
|
+
5. Treat the generated output as the entire authoritative file.
|
|
172
|
+
6. Do **not** patch a single section, append new text to the end, or leave untouched legacy sections in place.
|
|
173
|
+
7. Keep the output valid Markdown.
|
|
174
|
+
8. When writing, replace the old file contents in one operation.
|
|
145
175
|
|
|
146
|
-
###
|
|
176
|
+
### 9. Generate `README.md` for the project-overview objective
|
|
147
177
|
|
|
148
178
|
When `target_path = README.md`, generate the full document around the resolved project-overview contract.
|
|
149
179
|
|
|
@@ -175,7 +205,7 @@ When `target_path = README.md`, generate the full document around the resolved p
|
|
|
175
205
|
- Preserve useful setup warnings from `existing_intent` when they are still valid.
|
|
176
206
|
- Output valid Markdown lists or numbered steps.
|
|
177
207
|
|
|
178
|
-
###
|
|
208
|
+
### 10. Conditionally include a README Examples section
|
|
179
209
|
|
|
180
210
|
Only add `## Examples` to `README.md` when the synthesis context supports a grounded project-type inference.
|
|
181
211
|
|
|
@@ -212,7 +242,7 @@ Choose the strongest evidenced type in this priority order when multiple types a
|
|
|
212
242
|
- Do **not** include placeholder examples, pseudo-commands, or guessed endpoints.
|
|
213
243
|
- If you cannot produce two grounded examples, omit the section instead of improvising.
|
|
214
244
|
|
|
215
|
-
###
|
|
245
|
+
### 11. Generate non-README targets from the rule table
|
|
216
246
|
|
|
217
247
|
For every known non-README target, the path-to-objective rule table determines the file structure. Generate a full document that satisfies the matched rule.
|
|
218
248
|
|
|
@@ -309,6 +339,6 @@ Rules:
|
|
|
309
339
|
- Summarize each entry from commit subjects and, when needed, nearby commit context.
|
|
310
340
|
- Keep newest entries first.
|
|
311
341
|
|
|
312
|
-
###
|
|
342
|
+
### 12. Continue using the resolved objective
|
|
313
343
|
|
|
314
344
|
After the target path and objective are resolved, use them as the contract for all subsequent `/doc` work. Known paths must bypass the ambiguity gate, and all later decisions about evidence gathering, scope, regeneration, and structure must honor the surfaced `target_path` and `objective` instead of inferring a different goal.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# `/doc` authoring notes
|
|
2
|
+
|
|
3
|
+
## `last_update` provenance
|
|
4
|
+
|
|
5
|
+
`/doc` writes a `last_update` frontmatter field at the top of generated documentation files. That value is derived from completed scrum-board items that explicitly declare they affected the target document.
|
|
6
|
+
|
|
7
|
+
The resolver is `skills/doc/scripts/resolve_last_update.py`. It scans:
|
|
8
|
+
|
|
9
|
+
- `project/board/epics/`
|
|
10
|
+
- `project/board/stories/`
|
|
11
|
+
- `project/board/tasks/`
|
|
12
|
+
|
|
13
|
+
A board item counts as provenance only when all of the following are true:
|
|
14
|
+
|
|
15
|
+
1. `status` is `Done` or `Passed`
|
|
16
|
+
2. `docs` is a YAML list
|
|
17
|
+
3. The `docs` list contains the target file path as an exact repo-relative match (for example `README.md` or `docs/API.md`)
|
|
18
|
+
4. `date_completed` exists and parses as `YYYY-MM-DD`
|
|
19
|
+
|
|
20
|
+
If multiple board items match, `/doc` uses the most recent `date_completed` as `last_update`.
|
|
21
|
+
|
|
22
|
+
## Required board annotations
|
|
23
|
+
|
|
24
|
+
For provenance to work, the scrum-board item that changed a documentation file must carry a matching `docs: [...]` annotation in its YAML frontmatter.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
docs:
|
|
30
|
+
- README.md
|
|
31
|
+
- docs/API.md
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use repo-relative paths only. The resolver does not normalize absolute paths or `./`-prefixed variants.
|
|
35
|
+
|
|
36
|
+
## Fallback behavior
|
|
37
|
+
|
|
38
|
+
When `/doc` cannot determine provenance, it still emits valid YAML frontmatter and sets:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
last_update: unknown
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This fallback is intentional. It keeps the frontmatter shape stable while making the missing provenance explicit to both humans and downstream tooling.
|
|
45
|
+
|
|
46
|
+
## Failure modes and assumptions
|
|
47
|
+
|
|
48
|
+
### Board item completed without `docs` annotations
|
|
49
|
+
|
|
50
|
+
If a task, story, or epic reached `Done`/`Passed` but omitted the target file from `docs`, `/doc` cannot attribute that item to the document. The resolver will ignore it and may fall back to `unknown`.
|
|
51
|
+
|
|
52
|
+
### Target path mismatch
|
|
53
|
+
|
|
54
|
+
Matching is exact and repo-relative. These examples do **not** match `README.md`:
|
|
55
|
+
|
|
56
|
+
- `/Users/sam/.../README.md`
|
|
57
|
+
- `./README.md`
|
|
58
|
+
- `docs/../README.md`
|
|
59
|
+
|
|
60
|
+
Authors should record the canonical repo-relative path that `/doc` was invoked with.
|
|
61
|
+
|
|
62
|
+
### Item not in a completed status
|
|
63
|
+
|
|
64
|
+
Items in statuses such as `Pending`, `In Progress`, `Blocked`, or `Failed` do not count as provenance even if they include the right `docs` annotation. Only completed work is eligible.
|
|
65
|
+
|
|
66
|
+
### Missing or invalid `date_completed`
|
|
67
|
+
|
|
68
|
+
A matching board item without a valid `date_completed` cannot contribute a `last_update` value. The resolver skips it and logs a warning to stderr.
|
|
69
|
+
|
|
70
|
+
### Malformed legacy board files
|
|
71
|
+
|
|
72
|
+
Some older board artifacts may not follow the current YAML-frontmatter schema. The resolver skips unparseable files instead of failing the whole `/doc` run. This is a degradation path, not successful provenance.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve /doc last_update provenance from scrum board annotations."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import date
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
COMPLETED_STATUSES = {"Done", "Passed"}
|
|
14
|
+
BOARD_DIRS = (
|
|
15
|
+
("epic", Path("project/board/epics")),
|
|
16
|
+
("story", Path("project/board/stories")),
|
|
17
|
+
("task", Path("project/board/tasks")),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_args() -> argparse.Namespace:
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
description="Resolve the latest completed board date for a documentation target.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("target_path", help="Repo-relative documentation target path, e.g. README.md")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--root",
|
|
28
|
+
default=Path(__file__).resolve().parents[3],
|
|
29
|
+
type=Path,
|
|
30
|
+
help="Repository root containing project/board/",
|
|
31
|
+
)
|
|
32
|
+
return parser.parse_args()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_scalar(value: str) -> Any:
|
|
36
|
+
text = value.strip()
|
|
37
|
+
if text in {"", "null", "~"}:
|
|
38
|
+
return ""
|
|
39
|
+
if text.startswith("[") and text.endswith("]"):
|
|
40
|
+
inner = text[1:-1].strip()
|
|
41
|
+
if not inner:
|
|
42
|
+
return []
|
|
43
|
+
return [item.strip().strip("\"'") for item in inner.split(",") if item.strip()]
|
|
44
|
+
return text.strip("\"'")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_frontmatter(path: Path) -> dict[str, Any]:
|
|
48
|
+
text = path.read_text(encoding="utf-8")
|
|
49
|
+
if not text.startswith("---\n"):
|
|
50
|
+
raise ValueError("missing opening frontmatter delimiter")
|
|
51
|
+
parts = text.split("\n---\n", 1)
|
|
52
|
+
if len(parts) != 2:
|
|
53
|
+
raise ValueError("missing closing frontmatter delimiter")
|
|
54
|
+
|
|
55
|
+
lines = parts[0].splitlines()[1:]
|
|
56
|
+
data: dict[str, Any] = {}
|
|
57
|
+
current_key: str | None = None
|
|
58
|
+
|
|
59
|
+
for line in lines:
|
|
60
|
+
if not line.strip():
|
|
61
|
+
continue
|
|
62
|
+
if line.startswith(" - "):
|
|
63
|
+
if current_key is None:
|
|
64
|
+
raise ValueError(f"orphaned list item: {line.strip()}")
|
|
65
|
+
existing = data.get(current_key, "")
|
|
66
|
+
if existing == "":
|
|
67
|
+
existing = []
|
|
68
|
+
data[current_key] = existing
|
|
69
|
+
if not isinstance(existing, list):
|
|
70
|
+
raise ValueError(f"frontmatter key {current_key!r} is not a list")
|
|
71
|
+
existing.append(line[4:].strip().strip("\"'"))
|
|
72
|
+
continue
|
|
73
|
+
if ":" not in line:
|
|
74
|
+
raise ValueError(f"invalid frontmatter line: {line}")
|
|
75
|
+
key, raw_value = line.split(":", 1)
|
|
76
|
+
key = key.strip()
|
|
77
|
+
value = parse_scalar(raw_value)
|
|
78
|
+
data[key] = value
|
|
79
|
+
current_key = key if value == "" else None
|
|
80
|
+
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def iter_matches(root: Path, target_path: str):
|
|
85
|
+
for item_type, relative_dir in BOARD_DIRS:
|
|
86
|
+
board_dir = root / relative_dir
|
|
87
|
+
if not board_dir.exists():
|
|
88
|
+
continue
|
|
89
|
+
for path in sorted(board_dir.glob("*.md")):
|
|
90
|
+
try:
|
|
91
|
+
frontmatter = parse_frontmatter(path)
|
|
92
|
+
except Exception as exc: # pragma: no cover - defensive degradation
|
|
93
|
+
print(f"warning: skipping {path}: {exc}", file=sys.stderr)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if frontmatter.get("status") not in COMPLETED_STATUSES:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
docs = frontmatter.get("docs")
|
|
100
|
+
if not isinstance(docs, list) or target_path not in docs:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
completed_raw = str(frontmatter.get("date_completed", "")).strip()
|
|
104
|
+
if not completed_raw:
|
|
105
|
+
print(
|
|
106
|
+
f"warning: skipping {path}: matching docs annotation without date_completed",
|
|
107
|
+
file=sys.stderr,
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
completed_on = date.fromisoformat(completed_raw)
|
|
113
|
+
except ValueError:
|
|
114
|
+
print(
|
|
115
|
+
f"warning: skipping {path}: invalid date_completed {completed_raw!r}",
|
|
116
|
+
file=sys.stderr,
|
|
117
|
+
)
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
yield {
|
|
121
|
+
"id": frontmatter.get("id") or path.stem,
|
|
122
|
+
"type": item_type,
|
|
123
|
+
"status": frontmatter.get("status"),
|
|
124
|
+
"date_completed": completed_on.isoformat(),
|
|
125
|
+
"path": path.relative_to(root).as_posix(),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> int:
|
|
130
|
+
args = parse_args()
|
|
131
|
+
root = args.root.resolve()
|
|
132
|
+
matches = sorted(
|
|
133
|
+
iter_matches(root, args.target_path),
|
|
134
|
+
key=lambda item: (item["date_completed"], item["id"]),
|
|
135
|
+
)
|
|
136
|
+
last_update = matches[-1]["date_completed"] if matches else "unknown"
|
|
137
|
+
payload = {
|
|
138
|
+
"target_path": args.target_path,
|
|
139
|
+
"last_update": last_update,
|
|
140
|
+
"provenance_found": bool(matches),
|
|
141
|
+
"fallback": "unknown",
|
|
142
|
+
"matched_items": matches,
|
|
143
|
+
}
|
|
144
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
raise SystemExit(main())
|