@davidorex/pi-workflows 0.14.6 → 0.26.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.
Files changed (79) hide show
  1. package/README.md +12 -9
  2. package/agents/handoff-writer.agent.yaml +1 -1
  3. package/dist/agent-spec.d.ts.map +1 -1
  4. package/dist/agent-spec.js +4 -3
  5. package/dist/agent-spec.js.map +1 -1
  6. package/dist/bundled-dirs.d.ts +2 -0
  7. package/dist/bundled-dirs.d.ts.map +1 -0
  8. package/dist/bundled-dirs.js +21 -0
  9. package/dist/bundled-dirs.js.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +118 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/render-by-id.d.ts +62 -0
  15. package/dist/render-by-id.d.ts.map +1 -0
  16. package/dist/render-by-id.js +102 -0
  17. package/dist/render-by-id.js.map +1 -0
  18. package/dist/step-agent.js +1 -1
  19. package/dist/step-block.d.ts +5 -1
  20. package/dist/step-block.d.ts.map +1 -1
  21. package/dist/step-block.js +88 -40
  22. package/dist/step-block.js.map +1 -1
  23. package/dist/step-monitor.d.ts +1 -1
  24. package/dist/step-monitor.d.ts.map +1 -1
  25. package/dist/step-monitor.js +1 -1
  26. package/dist/step-monitor.js.map +1 -1
  27. package/dist/step-shared.d.ts +1 -1
  28. package/dist/step-shared.d.ts.map +1 -1
  29. package/dist/step-shared.js +16 -10
  30. package/dist/step-shared.js.map +1 -1
  31. package/dist/template-validation.d.ts.map +1 -1
  32. package/dist/template-validation.js +5 -5
  33. package/dist/template-validation.js.map +1 -1
  34. package/dist/template.d.ts.map +1 -1
  35. package/dist/template.js +2 -1
  36. package/dist/template.js.map +1 -1
  37. package/dist/test-helpers.d.ts +91 -0
  38. package/dist/test-helpers.d.ts.map +1 -1
  39. package/dist/test-helpers.js +186 -1
  40. package/dist/test-helpers.js.map +1 -1
  41. package/dist/tui.d.ts +3 -3
  42. package/dist/tui.d.ts.map +1 -1
  43. package/dist/tui.js +1 -1
  44. package/dist/types.d.ts +36 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/workflow-discovery.d.ts.map +1 -1
  47. package/dist/workflow-discovery.js +2 -1
  48. package/dist/workflow-discovery.js.map +1 -1
  49. package/dist/workflow-executor.d.ts.map +1 -1
  50. package/dist/workflow-executor.js +23 -9
  51. package/dist/workflow-executor.js.map +1 -1
  52. package/dist/workflow-sdk.d.ts.map +1 -1
  53. package/dist/workflow-sdk.js +14 -8
  54. package/dist/workflow-sdk.js.map +1 -1
  55. package/dist/workflow-spec.js +99 -2
  56. package/dist/workflow-spec.js.map +1 -1
  57. package/package.json +9 -5
  58. package/skill-narrative.md +3 -1
  59. package/skills/pi-workflows/SKILL.md +43 -6
  60. package/skills/pi-workflows/references/bundled-resources.md +16 -1
  61. package/templates/items/architecture.md +33 -0
  62. package/templates/items/conformance.md +26 -0
  63. package/templates/items/conventions.md +19 -0
  64. package/templates/items/decisions.md +47 -0
  65. package/templates/items/domain.md +21 -0
  66. package/templates/items/features.md +63 -0
  67. package/templates/items/framework-gaps.md +43 -0
  68. package/templates/items/issues.md +21 -0
  69. package/templates/items/layer-plans.md +50 -0
  70. package/templates/items/project.md +48 -0
  71. package/templates/items/requirements.md +20 -0
  72. package/templates/items/research.md +57 -0
  73. package/templates/items/spec-reviews.md +51 -0
  74. package/templates/items/tasks.md +29 -0
  75. package/templates/shared/macros.md +83 -98
  76. package/templates/shared/render-helpers.md +106 -0
  77. package/workflows/analyze-existing-project.workflow.yaml +1 -1
  78. package/workflows/init-new-project.workflow.yaml +1 -1
  79. package/workflows/plan-from-requirements.workflow.yaml +2 -2
@@ -0,0 +1,48 @@
1
+ {#-
2
+ Per-item macro for the `project` block (Plan 8 / per-item-macros wave 4).
3
+
4
+ Singleton kind: the project block holds one record per repository (no
5
+ homogeneous items[] array). The "per-item" rendering thus operates on the
6
+ whole block payload as a single record. The whole-block delegator in
7
+ shared/macros.md passes its data argument straight through.
8
+
9
+ No cross-block references — depth parameter accepted for shape-uniformity
10
+ with other per-item macros; no helpers from shared/render-helpers.md are
11
+ needed at present.
12
+
13
+ Empty-array convention:
14
+ Optional list/object fields render their label only when populated. The
15
+ project schema makes `name`, `description`, and `core_value` required and
16
+ the rest optional; bare-required output is the predictable shape for
17
+ minimal records.
18
+ -#}
19
+
20
+ {% macro render_project_item(p, depth=0) %}
21
+ {% if p %}
22
+ ## Project
23
+ **{{ p.name }}** — {{ enforceBudget(p.description, "project", "description") }}
24
+ {% if p.core_value %}Core value: {{ enforceBudget(p.core_value, "project", "core_value") }}{% endif %}
25
+ {% if p.vision %}
26
+ Vision: {{ enforceBudget(p.vision, "project", "vision") }}{% endif %}
27
+ {% if p.status %}Status: {{ p.status }}{% endif %}
28
+ {% if p.target_users %}
29
+ Target users: {{ p.target_users | join(", ") }}{% endif %}
30
+ {% if p.constraints %}
31
+ ### Constraints
32
+ {% for c in p.constraints %}- [{{ c.type }}] {{ enforceBudget(c.description, "project", "constraints.items.description") }}
33
+ {% endfor %}{% endif %}
34
+ {% if p.scope_boundaries %}
35
+ ### Scope
36
+ **In:** {% for s in p.scope_boundaries.in %}{{ s }}{% if not loop.last %}, {% endif %}{% endfor %}
37
+
38
+ **Out:** {% for s in p.scope_boundaries.out %}{{ s }}{% if not loop.last %}, {% endif %}{% endfor %}
39
+ {% endif %}
40
+ {% if p.goals %}
41
+ ### Goals
42
+ {% for g in p.goals %}- **{{ g.id }}**: {{ enforceBudget(g.description, "project", "goals.items.description") }}{% if g.success_criteria %} — criteria: {% for sc in g.success_criteria %}{{ enforceBudget(sc, "project", "goals.items.success_criteria.items") }}{% if not loop.last %}; {% endif %}{% endfor %}{% endif %}
43
+ {% endfor %}{% endif %}
44
+ {% if p.repository %}Repository: {{ p.repository }}
45
+ {% endif %}{% if p.stack %}Stack: {{ p.stack | join(", ") }}
46
+ {% endif %}
47
+ {% endif %}
48
+ {% endmacro %}
@@ -0,0 +1,20 @@
1
+ {#-
2
+ Per-item macro for the `requirements` block (Plan 8 / per-item-macros wave 4).
3
+
4
+ Block schema: .project/schemas/requirements.schema.json. Each item is one
5
+ requirement inside data.requirements[]. Required: id (^REQ-\d{3,}$),
6
+ description, type, status, priority. Optional: acceptance_criteria,
7
+ source, traces_to (phase/task IDs), depends_on (other requirement IDs).
8
+
9
+ Cross-block reference recursion (traces_to, depends_on) is delegated to
10
+ shared/render-helpers.md — see that file for the depth contract, ambient
11
+ globals, and empty-array convention shared across all per-item macros.
12
+ -#}
13
+ {% from "shared/render-helpers.md" import render_id_list_inline %}
14
+
15
+ {% macro render_requirement(req, depth=0) %}
16
+ {% if req %}- **{{ req.id }}** [{{ req.priority }}] ({{ req.type }}, {{ req.status }}): {{ enforceBudget(req.description, "requirements", "requirements.items.description") }}{% if req.acceptance_criteria %}
17
+ Criteria: {% for ac in req.acceptance_criteria %}{{ enforceBudget(ac, "requirements", "requirements.items.acceptance_criteria.items") }}{% if not loop.last %}; {% endif %}{% endfor %}{% endif %}
18
+ {% if req.source %} Source: {{ req.source }}
19
+ {% endif %}{{ render_id_list_inline("Traces to", req.traces_to, depth) }}{{ render_id_list_inline("Depends on", req.depends_on, depth) }}{% endif %}
20
+ {% endmacro %}
@@ -0,0 +1,57 @@
1
+ {#-
2
+ Per-item macro for the `research` block.
3
+
4
+ Block schema: .project/schemas/research.schema.json (Plan 7 / per-item-macros wave 4).
5
+
6
+ Cross-block reference recursion (informs, informed_by, related_research,
7
+ produces_findings, supersedes, superseded_by) is delegated to
8
+ shared/render-helpers.md — see that file for the depth contract, ambient
9
+ globals, and empty-array convention.
10
+
11
+ Embedded substructures:
12
+ `grounding` is an object with dependencies/revisions/external_refs
13
+ string arrays — rendered inline as labelled sub-lists, never recursed.
14
+ `citations[]` carries label/path/lines/url/retrieved_at — labelled
15
+ pointers to source material, not block IDs; rendered inline.
16
+ `stale_conditions[]` and `scope[]` are simple string arrays rendered as
17
+ bullet sub-lists.
18
+
19
+ x-prompt-budget integration:
20
+ The schema annotates `findings_summary`, `question`, `method` with
21
+ `x-prompt-budget`. enforceBudget is wired for these per Plan 5.
22
+ -#}
23
+ {% from "shared/render-helpers.md" import render_id_list_block, render_id_single_block, render_optional_scalar %}
24
+
25
+ {% macro render_research(r, depth=0) %}
26
+ ID: {{ r.id }}
27
+ Title: {{ r.title }}
28
+ Status: {{ r.status }}
29
+ Layer: {{ r.layer }}
30
+ Type: {{ r.type }}
31
+ Created by: {{ r.created_by }}
32
+ Created at: {{ r.created_at }}
33
+ {{ render_optional_scalar("Modified by", r.modified_by) }}{{ render_optional_scalar("Modified at", r.modified_at) }}{{ render_optional_scalar("Conducted by", r.conducted_by) }}{{ render_optional_scalar("Conducted at", r.conducted_at) }}{{ render_optional_scalar("Grounded at", r.grounded_at) }}
34
+ Question:
35
+ {{ enforceBudget(r.question, "research", "research.items.question") }}
36
+
37
+ Method:
38
+ {{ enforceBudget(r.method, "research", "research.items.method") }}
39
+
40
+ {% if r.scope is defined %}Scope:
41
+ {% if r.scope | length > 0 %}{% for s in r.scope %} - {{ s }}
42
+ {% endfor %}{% else %} (none)
43
+ {% endif %}{% endif %}Findings summary:
44
+ {{ enforceBudget(r.findings_summary, "research", "research.items.findings_summary") }}
45
+
46
+ {% if r.findings_document %}Findings document: {{ r.findings_document }}
47
+ {% endif %}{% if r.grounding is defined %}Grounding:
48
+ {% if r.grounding.dependencies is defined %} Dependencies: {% if r.grounding.dependencies | length > 0 %}{% for d in r.grounding.dependencies %}{{ d }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}(none){% endif %}
49
+ {% endif %}{% if r.grounding.revisions is defined %} Revisions: {% if r.grounding.revisions | length > 0 %}{% for rv in r.grounding.revisions %}{{ rv }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}(none){% endif %}
50
+ {% endif %}{% if r.grounding.external_refs is defined %} External refs: {% if r.grounding.external_refs | length > 0 %}{% for er in r.grounding.external_refs %}{{ er }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}(none){% endif %}
51
+ {% endif %}{% endif %}{% if r.stale_conditions is defined %}Stale conditions:
52
+ {% if r.stale_conditions | length > 0 %}{% for sc in r.stale_conditions %} - {{ sc }}
53
+ {% endfor %}{% else %} (none)
54
+ {% endif %}{% endif %}{% if r.citations is defined %}Citations:
55
+ {% if r.citations | length > 0 %}{% for c in r.citations %} - {{ c.label }}{% if c.path %} — path: {{ c.path }}{% if c.lines %}:{{ c.lines }}{% endif %}{% endif %}{% if c.url %} — url: {{ c.url }}{% endif %}{% if c.retrieved_at %} — retrieved_at: {{ c.retrieved_at }}{% endif %}
56
+ {% endfor %}{% else %} (none)
57
+ {% endif %}{% endif %}{{ render_id_list_block("Informs", r.informs, depth) }}{{ render_id_list_block("Informed by", r.informed_by, depth) }}{{ render_id_list_block("Related research", r.related_research, depth) }}{{ render_id_list_block("Produces findings", r.produces_findings, depth) }}{{ render_id_list_block("Supersedes", r.supersedes, depth) }}{{ render_id_single_block("Superseded by", r.superseded_by, depth) }}{% endmacro %}
@@ -0,0 +1,51 @@
1
+ {#-
2
+ Per-item macro for the `spec-reviews` block.
3
+
4
+ Block schema: .project/schemas/spec-reviews.schema.json (Plan 7 / per-item-macros wave 4).
5
+
6
+ Findings:
7
+ Each review carries an embedded findings registry (definitions/finding
8
+ in the schema). Findings are rendered inline as a sub-list under the
9
+ review body — they are not block items in their own right and have no
10
+ per-item macro of their own. Each finding's `produces_decision` (when
11
+ present and depth > 0) recurses into the decisions block via
12
+ resolve/render_recursive — that pattern stays inline because it has a
13
+ deeper indent (4 spaces) than the helper convention assumes and is
14
+ embedded in a finding-iteration loop rather than a top-level field.
15
+ `related_findings` on a finding refer to sibling findings within the
16
+ same review by their string ids; they are emitted as bare IDs (no
17
+ recursion — finding IDs are scoped to the review and not in the global
18
+ ID index).
19
+
20
+ Optional review-level scalars (target_revision, reviewer, completed_at,
21
+ clean) and the method body delegate to shared/render-helpers.md
22
+ render_optional_scalar where the shape matches.
23
+ -#}
24
+ {% from "shared/render-helpers.md" import render_optional_scalar %}
25
+
26
+ {% macro render_spec_review(rev, depth=0) %}
27
+ ID: {{ rev.id }}
28
+ Target: {{ rev.target }}
29
+ Status: {{ rev.status }}
30
+ Created by: {{ rev.created_by }}
31
+ Created at: {{ rev.created_at }}
32
+ {{ render_optional_scalar("Target revision", rev.target_revision) }}{{ render_optional_scalar("Reviewer", rev.reviewer) }}{{ render_optional_scalar("Completed at", rev.completed_at) }}{% if rev.clean is defined %}Clean: {{ rev.clean }}
33
+ {% endif %}{% if rev.method %}Method:
34
+ {{ enforceBudget(rev.method, "spec-reviews", "reviews.items.method") }}
35
+
36
+ {% endif %}{% if rev.scope is defined %}Scope:
37
+ {% if rev.scope | length > 0 %}{% for s in rev.scope %} - {{ s }}
38
+ {% endfor %}{% else %} (none)
39
+ {% endif %}{% endif %}Findings:
40
+ {% if rev.findings and rev.findings | length > 0 %}{% for f in rev.findings %} - {{ f.id }} [{{ f.severity }}/{{ f.category }}/{{ f.state }}] reporter={{ f.reporter }} at={{ f.created_at }}
41
+ Description: {{ enforceBudget(f.description, "spec-reviews", "reviews.items.findings.items.description") }}
42
+ {% if f.evidence %} Evidence: {{ f.evidence }}
43
+ {% endif %}{% if f.location %} Location: {{ f.location }}
44
+ {% endif %}{% if f.resolution %} Resolution: {{ f.resolution }}
45
+ {% endif %}{% if f.resolved_by %} Resolved by: {{ f.resolved_by }}
46
+ {% endif %}{% if f.resolved_at %} Resolved at: {{ f.resolved_at }}
47
+ {% endif %}{% if f.produces_decision %} Produces decision: {% if depth > 0 %}{% set loc = resolve(f.produces_decision) %}{% if loc %}
48
+ {{ render_recursive(loc, depth - 1) }}{% else %}{{ f.produces_decision }}{% endif %}{% else %}{{ f.produces_decision }}{% endif %}
49
+ {% endif %}{% if f.related_findings is defined %} Related findings: {% if f.related_findings | length > 0 %}{% for rf in f.related_findings %}{{ rf }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}(none){% endif %}
50
+ {% endif %}{% endfor %}{% else %} (none)
51
+ {% endif %}{% endmacro %}
@@ -0,0 +1,29 @@
1
+ {#-
2
+ Per-item macro for the `tasks` block (Plan 8 / per-item-macros wave 4).
3
+
4
+ Block schema: .project/schemas/tasks.schema.json. Each item is one task
5
+ inside data.tasks[]. Required: id (^TASK-\d{3,}$), description, status.
6
+ Optional: phase (string|integer), files, acceptance_criteria, depends_on
7
+ (other task IDs), assigned_agent, verification (verification ID), notes.
8
+
9
+ When status === "completed", schema additionally requires verification
10
+ (the if/then conditional in the schema).
11
+
12
+ Cross-block reference recursion (depends_on, verification) is delegated to
13
+ shared/render-helpers.md — see that file for the depth contract, ambient
14
+ globals, and empty-array convention. verification is rendered as a single
15
+ inline reference (one ID) rather than a list.
16
+
17
+ Files field:
18
+ files[] is a list of code paths, not project-block IDs. Rendered inline
19
+ as a comma-separated list; never recursed.
20
+ -#}
21
+ {% from "shared/render-helpers.md" import render_id_list_inline, render_id_single_inline %}
22
+
23
+ {% macro render_task(t, depth=0) %}
24
+ {% if t %}- **{{ t.id }}** [{{ t.status }}]: {{ enforceBudget(t.description, "tasks", "tasks.items.description") }}{% if t.phase %} (phase {{ t.phase }}){% endif %}{% if t.files %} — files: {{ t.files | join(", ") }}{% endif %}
25
+ {% if t.acceptance_criteria %} Criteria: {% for ac in t.acceptance_criteria %}{{ enforceBudget(ac, "tasks", "tasks.items.acceptance_criteria.items") }}{% if not loop.last %}; {% endif %}{% endfor %}
26
+ {% endif %}{% if t.assigned_agent %} Assigned agent: {{ t.assigned_agent }}
27
+ {% endif %}{% if t.notes %} Notes: {{ enforceBudget(t.notes, "tasks", "tasks.items.notes") }}
28
+ {% endif %}{{ render_id_list_inline("Depends on", t.depends_on, depth) }}{{ render_id_single_inline("Verification", t.verification, depth) }}{% endif %}
29
+ {% endmacro %}
@@ -1,109 +1,104 @@
1
- {#- Block rendering macros — one per schema with agent prompt use cases.
2
- Import via: {% from "shared/macros.md" import render_project, render_architecture %}
3
- Each macro: null guard, markdown heading, field rendering. Missing data renders nothing. -#}
1
+ {#- Block rendering macros — whole-block forms as derived views over per-item macros.
4
2
 
5
- {% macro render_project(data) %}
6
- {% if data %}
7
- ## Project
8
- **{{ data.name }}** {{ data.description }}
9
- {% if data.core_value %}Core value: {{ data.core_value }}{% endif %}
10
- {% if data.vision %}
11
- Vision: {{ data.vision }}{% endif %}
12
- {% if data.status %}Status: {{ data.status }}{% endif %}
13
- {% if data.target_users %}
14
- Target users: {{ data.target_users | join(", ") }}{% endif %}
15
- {% if data.constraints %}
16
- ### Constraints
17
- {% for c in data.constraints %}- [{{ c.type }}] {{ c.description }}
18
- {% endfor %}{% endif %}
19
- {% if data.scope_boundaries %}
20
- ### Scope
21
- **In:** {% for s in data.scope_boundaries.in %}{{ s }}{% if not loop.last %}, {% endif %}{% endfor %}
3
+ Plan 8 (Wave 4) refactor: each whole-block macro that has a per-item sibling
4
+ in templates/items/<kind>.md delegates to that per-item macro by mapping
5
+ over the items array. Per-item macros own field rendering; whole-block macros
6
+ own the surrounding heading and the null/empty guard.
22
7
 
23
- **Out:** {% for s in data.scope_boundaries.out %}{{ s }}{% if not loop.last %}, {% endif %}{% endfor %}
24
- {% endif %}
25
- {% if data.goals %}
26
- ### Goals
27
- {% for g in data.goals %}- **{{ g.id }}**: {{ g.description }}{% if g.success_criteria %} — criteria: {{ g.success_criteria | join("; ") }}{% endif %}
28
- {% endfor %}{% endif %}
29
- {% endif %}
8
+ v0.24.2 follow-up (Item 3 of 2026-05-02 residual-debt patch): the
9
+ repeated `{% if data and data.<key> [and data.<key> | length > 0] %}` +
10
+ `## <Heading>` + `{% for x in data.<key> %}{{ render_<kind>_item(x) }}
11
+ {% endfor %}` shape that previously lived inline in render_requirements,
12
+ render_domain, render_tasks, and render_issues now routes through the
13
+ `render_whole_block_truthy` / `render_whole_block_nonempty` scaffolds in
14
+ shared/render-helpers.md. The guard / heading / loop chassis lives once
15
+ in render-helpers; each whole-block macro here is the per-kind binding
16
+ (heading text, items slice, per-item macro inside the `{% call %}` body).
17
+ Output stays byte-identical to the prior inline shape — the two
18
+ truthy-vs-nonempty helpers preserve the empty-array surface each
19
+ delegator originally shipped (tasks/issues/domain emit `## <Heading>`
20
+ with empty array; requirements emits nothing — both regimes are kept
21
+ explicitly so existing consumers see no diff).
22
+
23
+ Imports for downstream consumers (unchanged signatures):
24
+ {% from "shared/macros.md" import render_project, render_architecture,
25
+ render_requirements, render_conformance,
26
+ render_domain, render_tasks, render_issues,
27
+ render_conventions,
28
+ render_exploration, render_exploration_full %}
29
+
30
+ Each macro:
31
+ - null/empty guard via the helper or local conditional,
32
+ - markdown heading (via the helper for list kinds; inline for the
33
+ singletons and the heading-bearing block-level shapes),
34
+ - delegation loop calling the per-item macro per item.
35
+
36
+ Retired in Plan 8:
37
+ - render_decisions(data) — legacy whole-block decisions macro deleted.
38
+ The new lifecycle decisions block has its own per-item macro
39
+ (templates/items/decisions.md ships render_decision + alias
40
+ render_decisions). The legacy macro rendered fields (decision,
41
+ rationale) that no longer match the current decisions.schema.json.
42
+ - render_gap(gap) — legacy single-item validation-concept macro deleted.
43
+ The framework-gaps.json block uses Plan 7's render_framework_gap in
44
+ templates/items/framework-gaps.md.
45
+
46
+ Retained as-is:
47
+ - render_exploration(exploration) and render_exploration_full(exploration)
48
+ are workflow-step output formatters (rendering exploration payloads
49
+ emitted by exploration agents), not project-block renderers. They do
50
+ not fit the per-item refactor pattern and stay unchanged. -#}
51
+
52
+ {% from "shared/render-helpers.md" import render_whole_block_truthy, render_whole_block_nonempty %}
53
+
54
+ {% from "items/project.md" import render_project_item %}
55
+ {% macro render_project(data) %}
56
+ {% if data %}{{ render_project_item(data) }}{% endif %}
30
57
  {% endmacro %}
31
58
 
59
+ {% from "items/architecture.md" import render_architecture_item %}
32
60
  {% macro render_architecture(data) %}
33
- {% if data %}
34
- ## Architecture
35
- {% if data.overview %}{{ data.overview }}
36
- {% endif %}
37
- {% if data.modules %}
38
- ### Modules
39
- {% for m in data.modules %}- **{{ m.name }}** (`{{ m.file }}`{% if m.lines %}, {{ m.lines }} lines{% endif %}): {{ m.responsibility }}{% if m.dependencies %} — deps: {{ m.dependencies | join(", ") }}{% endif %}
40
- {% endfor %}{% endif %}
41
- {% if data.patterns %}
42
- ### Patterns
43
- {% for p in data.patterns %}- **{{ p.name }}**: {{ p.description }}{% if p.used_in %} — used in: {{ p.used_in | join(", ") }}{% endif %}
44
- {% endfor %}{% endif %}
45
- {% if data.boundaries %}
46
- ### Boundaries
47
- {% for b in data.boundaries %}- {{ b }}
48
- {% endfor %}{% endif %}
49
- {% endif %}
61
+ {% if data %}{{ render_architecture_item(data) }}{% endif %}
50
62
  {% endmacro %}
51
63
 
52
- {% macro render_requirements(data) %}
53
- {% if data and data.requirements and data.requirements | length > 0 %}
54
- ## Requirements
55
- {% for r in data.requirements %}- **{{ r.id }}** [{{ r.priority }}] ({{ r.type }}, {{ r.status }}): {{ r.description }}{% if r.acceptance_criteria %}
56
- Criteria: {{ r.acceptance_criteria | join("; ") }}{% endif %}
57
- {% endfor %}
58
- {% endif %}
59
- {% endmacro %}
64
+ {% from "items/requirements.md" import render_requirement %}
65
+ {% macro render_requirements(data) %}{% call(req) render_whole_block_nonempty("Requirements", data.requirements if data else None) %}{{ render_requirement(req) }}{% endcall %}{% endmacro %}
60
66
 
67
+ {% from "items/conformance.md" import render_conformance_principle %}
61
68
  {% macro render_conformance(data) %}
62
69
  {% if data and data.principles %}
63
70
  ## Conformance Reference
64
71
  {% if data.name %}**{{ data.name }}**{% endif %}
65
- {% for p in data.principles %}
66
- ### {{ p.id }}: {{ p.name }}
67
- {% if p.description %}{{ p.description }}{% endif %}
68
- {% for r in p.rules %}- **{{ r.id }}**: {{ r.rule }}{% if r.severity %} [{{ r.severity }}]{% endif %}{% if r.check_method %} — check: {{ r.check_method }}{% endif %}
69
- {% if r.anti_patterns %} Anti-patterns: {{ r.anti_patterns | join("; ") }}
70
- {% endif %}{% endfor %}
71
- {% endfor %}
72
+ {% for p in data.principles %}{{ render_conformance_principle(p) }}{% endfor %}
72
73
  {% endif %}
73
74
  {% endmacro %}
74
75
 
75
- {% macro render_domain(data) %}
76
- {% if data and data.entries %}
77
- ## Domain Knowledge
78
- {% for e in data.entries %}- **{{ e.id }}** [{{ e.category }}]: {{ e.title }}
79
- {{ e.content }}{% if e.tags %} — tags: {{ e.tags | join(", ") }}{% endif %}
80
- {% endfor %}
81
- {% endif %}
82
- {% endmacro %}
76
+ {% from "items/domain.md" import render_domain_entry %}
77
+ {% macro render_domain(data) %}{% call(e) render_whole_block_truthy("Domain Knowledge", data.entries if data else None) %}{{ render_domain_entry(e) }}{% endcall %}{% endmacro %}
83
78
 
84
- {% macro render_decisions(data) %}
85
- {% if data and data.decisions %}
86
- ## Decisions
87
- {% for d in data.decisions %}- **{{ d.id }}** ({{ d.status }}): {{ d.decision }}
88
- Rationale: {{ d.rationale }}{% if d.context %} — context: {{ d.context }}{% endif %}
89
- {% endfor %}
90
- {% endif %}
91
- {% endmacro %}
79
+ {% from "items/tasks.md" import render_task %}
80
+ {% macro render_tasks(data) %}{% call(t) render_whole_block_truthy("Tasks", data.tasks if data else None) %}{{ render_task(t) }}{% endcall %}{% endmacro %}
92
81
 
93
- {% macro render_tasks(data) %}
94
- {% if data and data.tasks %}
95
- ## Tasks
96
- {% for t in data.tasks %}- **{{ t.id }}** [{{ t.status }}]: {{ t.description }}{% if t.phase %} (phase {{ t.phase }}){% endif %}{% if t.files %} — files: {{ t.files | join(", ") }}{% endif %}
97
- {% endfor %}
98
- {% endif %}
99
- {% endmacro %}
82
+ {% from "items/issues.md" import render_issue %}
83
+ {% macro render_issues(data) %}{% call(i) render_whole_block_truthy("Issues", data.issues if data else None) %}{{ render_issue(i) }}{% endcall %}{% endmacro %}
100
84
 
101
- {% macro render_issues(data) %}
102
- {% if data and data.issues %}
103
- ## Issues
104
- {% for i in data.issues %}- **{{ i.id }}** [{{ i.priority }}, {{ i.status }}]: {{ i.title }}
105
- {{ i.body }}{% if i.location %} — {{ i.location }}{% endif %}{% if i.package %} ({{ i.package }}){% endif %}
106
- {% endfor %}
85
+ {#- Authored fresh in Plan 8: render_conventions did not previously exist in
86
+ macros.md despite README references implying it did. The block-level
87
+ header surfaces test_conventions, lint_command, and lint_scope (all
88
+ optional) before iterating rules through render_convention.
89
+
90
+ Block schema: .project/schemas/conventions.schema.json. Required: rules.
91
+ Optional: test_conventions { runner_command, file_pattern }, lint_command,
92
+ lint_scope. -#}
93
+ {% from "items/conventions.md" import render_convention %}
94
+ {% macro render_conventions(data) %}
95
+ {% if data and data.rules %}
96
+ ## Conventions
97
+ {% if data.test_conventions %}
98
+ **Tests:** `{{ data.test_conventions.runner_command }}` (pattern: `{{ data.test_conventions.file_pattern }}`)
99
+ {% endif %}{% if data.lint_command %}**Lint:** `{{ data.lint_command }}`{% if data.lint_scope %} (scope: {{ data.lint_scope }}){% endif %}
100
+ {% endif %}
101
+ {% for rule in data.rules %}{{ render_convention(rule) }}{% endfor %}
107
102
  {% endif %}
108
103
  {% endmacro %}
109
104
 
@@ -140,13 +135,3 @@ Target users: {{ data.target_users | join(", ") }}{% endif %}
140
135
  {% endfor %}{% endif %}
141
136
  {% endif %}
142
137
  {% endmacro %}
143
-
144
- {% macro render_gap(gap) %}
145
- {% if gap %}
146
- **ID:** {{ gap.id }}
147
- **Description:** {{ gap.description }}
148
- **Category:** {{ gap.category | default("unspecified") }}
149
- **Priority:** {{ gap.priority | default("unspecified") }}
150
- {% if gap.details %}**Details:** {{ gap.details }}{% endif %}
151
- {% endif %}
152
- {% endmacro %}
@@ -0,0 +1,106 @@
1
+ {#-
2
+ Shared rendering helpers for per-item macros.
3
+
4
+ Captures repeated micro-patterns that previously lived inline in every
5
+ per-item macro file under `items/*.md`. The aim is one definition per
6
+ pattern, consumed by every per-item macro that uses it.
7
+
8
+ Helpers exported:
9
+ render_id_list_block(label, ids, depth)
10
+ render_id_single_block(label, id, depth)
11
+ render_id_list_inline(label, ids, depth)
12
+ render_id_single_inline(label, id, depth)
13
+ render_optional_scalar(label, value)
14
+
15
+ Output convention:
16
+ Block-style helpers emit `<Label>:\n` followed by ` - <id>\n` per entry
17
+ (or ` (none)\n` for present-but-empty arrays). Absent (undefined)
18
+ arrays emit nothing.
19
+
20
+ Inline-style helpers emit ` <Label>: <id>[, <id>...]\n` (or `(none)\n`
21
+ for empty). The two-space indent prefix is part of the inline shape used
22
+ by domain.related_requirements, requirements.traces_to/depends_on, and
23
+ tasks.depends_on/verification — it is the indent under a parent list
24
+ bullet, not arbitrary padding.
25
+
26
+ Depth contract:
27
+ Each helper that takes `depth` honours the same recursion budget as the
28
+ per-item macros: depth <= 0 emits bare ID text; depth > 0 calls resolve
29
+ + render_recursive (ambient Nunjucks globals registered by compileAgent
30
+ in @davidorex/pi-jit-agents) and falls back to bare-ID on resolve miss.
31
+ Cycles → `[cycle: <id>]` and missing per-item macros → `[unrendered:
32
+ <kind>/<id>]` are produced inside render_recursive itself, not here.
33
+
34
+ resolve(id) and render_recursive(loc, depth) are env-level globals
35
+ available to imported macros without re-import.
36
+
37
+ Empty-array convention:
38
+ `is defined` discriminates absent vs present. Present-but-empty arrays
39
+ render the label with `(none)`; absent fields render nothing. This
40
+ preserves the per-item macro contract documented in items/decisions.md
41
+ and mirrored across every per-item macro.
42
+
43
+ Output equivalence:
44
+ The helper bodies are byte-identical to the patterns they replace. The
45
+ existing render-*.test.ts assertions are the regression net. If a
46
+ helper drifts from the inline pattern's output, those tests break.
47
+ -#}
48
+
49
+ {% macro render_id_list_block(label, ids, depth) %}{% if ids is defined %}{{ label }}:
50
+ {% if ids | length > 0 %}{% for id in ids %}{% if depth > 0 %}{% set loc = resolve(id) %}{% if loc %}{{ render_recursive(loc, depth - 1) }}
51
+ {% else %} - {{ id }}
52
+ {% endif %}{% else %} - {{ id }}
53
+ {% endif %}{% endfor %}{% else %} (none)
54
+ {% endif %}{% endif %}{% endmacro %}
55
+
56
+ {% macro render_id_single_block(label, id, depth) %}{% if id is defined and id %}{{ label }}:
57
+ {% if depth > 0 %}{% set loc = resolve(id) %}{% if loc %}{{ render_recursive(loc, depth - 1) }}
58
+ {% else %} - {{ id }}
59
+ {% endif %}{% else %} - {{ id }}
60
+ {% endif %}{% endif %}{% endmacro %}
61
+
62
+ {% macro render_id_list_inline(label, ids, depth) %}{% if ids is defined %} {{ label }}: {% if ids | length > 0 %}{% for id in ids %}{% if depth > 0 %}{% set loc = resolve(id) %}{% if loc %}{{ render_recursive(loc, depth - 1) }}{% else %}{{ id }}{% endif %}{% else %}{{ id }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}(none){% endif %}
63
+ {% endif %}{% endmacro %}
64
+
65
+ {% macro render_id_single_inline(label, id, depth) %}{% if id %} {{ label }}: {% if depth > 0 %}{% set loc = resolve(id) %}{% if loc %}{{ render_recursive(loc, depth - 1) }}{% else %}{{ id }}{% endif %}{% else %}{{ id }}{% endif %}
66
+ {% endif %}{% endmacro %}
67
+
68
+ {% macro render_optional_scalar(label, value) %}{% if value %}{{ label }}: {{ value }}
69
+ {% endif %}{% endmacro %}
70
+
71
+ {#-
72
+ Whole-block list scaffold (Plan 8 follow-up).
73
+
74
+ Centralises the truthy-guard + heading + per-item iteration shape that
75
+ previously repeated across the whole-block delegators in shared/macros.md
76
+ for `render_domain`, `render_tasks`, and `render_issues`. Each delegator
77
+ now reduces to one `{% call %}` block that supplies the per-item macro as
78
+ its body — the heading-and-loop chassis lives here once.
79
+
80
+ Two variants encode the two empty-handling regimes the legacy delegators
81
+ shipped, kept distinct so output stays byte-identical to the originals:
82
+
83
+ `render_whole_block_truthy(heading, items)`
84
+ Heading-and-loop fire whenever `items` is truthy (matches the
85
+ legacy `{% if data and data.<key> %}` guard used by tasks, issues,
86
+ domain). Empty arrays still emit the heading because `[]` is truthy
87
+ in Nunjucks — that surface is preserved deliberately.
88
+
89
+ `render_whole_block_nonempty(heading, items)`
90
+ Heading-and-loop fire only when `items | length > 0` (matches the
91
+ legacy `{% if data and data.requirements and data.requirements |
92
+ length > 0 %}` guard used by requirements). Empty arrays produce
93
+ nothing.
94
+
95
+ In both, the caller body (`{{ caller(item) }}`) is invoked once per item
96
+ with the item bound to the call-block parameter.
97
+ -#}
98
+ {% macro render_whole_block_truthy(heading, items) %}{% if items %}
99
+ ## {{ heading }}
100
+ {% for item in items %}{{ caller(item) }}{% endfor %}
101
+ {% endif %}{% endmacro %}
102
+
103
+ {% macro render_whole_block_nonempty(heading, items) %}{% if items and items | length > 0 %}
104
+ ## {{ heading }}
105
+ {% for item in items %}{{ caller(item) }}{% endfor %}
106
+ {% endif %}{% endmacro %}
@@ -85,4 +85,4 @@ completion:
85
85
  **Architecture:** ${{ steps.infer-architecture.output.modules | length }} modules, ${{ steps.infer-architecture.output.patterns | length }} patterns
86
86
  **Gaps:** ${{ steps.identify-issues.output.issues | length }} identified
87
87
 
88
- Use `/project status` to see the full state. Use `/project validate` to check cross-block references.
88
+ Use `/context status` to see the full state. Use `/context validate` to check cross-block references.
@@ -78,4 +78,4 @@ completion:
78
78
  **Architecture:** ${{ steps.design-architecture.output.modules | length }} modules
79
79
  **Plan:** phases and tasks created
80
80
 
81
- Use `/project status` to see the full state. Use `/project validate` to check cross-block references.
81
+ Use `/context status` to see the full state. Use `/context validate` to check cross-block references.
@@ -44,5 +44,5 @@ completion:
44
44
  template: |
45
45
  Plan created from requirements. Tasks written to .project/tasks.json.
46
46
 
47
- Use `/project status` to see the phase and task breakdown.
48
- Use `/project validate` to check cross-block references.
47
+ Use `/context status` to see the phase and task breakdown.
48
+ Use `/context validate` to check cross-block references.