@zalom/plastic 1.0.0-beta.1 → 1.0.0-beta.3
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/PLASTIC.md +14 -5
- package/agents/plastic-brainstorming.md +9 -1
- package/agents/plastic-executor.md +10 -0
- package/agents/plastic-planner.md +11 -1
- package/agents/plastic-spec-specialist.md +9 -1
- package/package.json +1 -1
- package/scripts/agent-report +142 -0
- package/scripts/dashboard.rb +5 -3
- package/scripts/doctor.rb +71 -0
- package/scripts/lib/intent_validator.rb +79 -0
- package/scripts/new-intent +50 -1
- package/scripts/spawn-preamble +18 -1
- package/skills/auto/SKILL.md +2 -0
- package/skills/auto/references/agent-architecture.md +20 -0
- package/skills/auto/references/agent-report-contract.md +86 -0
- package/skills/creating-intent/SKILL.md +20 -8
- package/skills/creating-intent/evals/evals.json +56 -0
- package/skills/creating-intent/references/lifecycle.md +7 -4
- package/skills/linking-intents/SKILL.md +11 -6
- package/skills/linking-intents/references/zettelkasten.md +8 -3
- package/skills/managing-index/references/zettelkasten-linking.md +1 -1
package/PLASTIC.md
CHANGED
|
@@ -35,15 +35,21 @@ Identity and knowledge graph only. Nothing operational.
|
|
|
35
35
|
---
|
|
36
36
|
id: "4a1"
|
|
37
37
|
intent: "Short description of the desire"
|
|
38
|
-
sources: ["4a"] #
|
|
39
|
-
chain: ["4a1a"] # forward
|
|
38
|
+
sources: ["4a"] # direct ascendants: intents this was created from
|
|
39
|
+
chain: ["4a1a"] # forward: what this spawned and related successors
|
|
40
40
|
created: 2026-05-29
|
|
41
41
|
author: human # human | agent-name
|
|
42
42
|
tags: [plastic, architecture]
|
|
43
43
|
---
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
- `sources`
|
|
46
|
+
- `sources` (formative, must-load, acyclic) and `chain` (forward + relational, lighter,
|
|
47
|
+
may cycle) form the directed knowledge graph. Reciprocity is one-directional: every
|
|
48
|
+
`sources` edge has a reciprocal `chain` entry (I1), but `chain` may carry relational
|
|
49
|
+
entries with no reciprocal `sources` (I2), so the graph is not strictly symmetric.
|
|
50
|
+
- Context contract: load `sources` strongly (they are what the intent was built from);
|
|
51
|
+
traverse `chain` lightly for discovery. See
|
|
52
|
+
docs/concepts/how-plastic-sources-and-chains-intents.md for the full model.
|
|
47
53
|
- IDs use Luhmann's alternating convention: `1` → `1a` → `1a1` → `1a1a`
|
|
48
54
|
- Multiple branches increment: `1a`, `1b`, `1c`
|
|
49
55
|
|
|
@@ -155,8 +161,11 @@ Format: `ID--three-to-five-words` (all stores).
|
|
|
155
161
|
|
|
156
162
|
- **Branch (`14a`, `14b`)** — a sub-task, refinement, or direct continuation of the
|
|
157
163
|
parent. It cannot stand on its own; it only makes sense as part of the parent's work.
|
|
158
|
-
- **Root (`15`, `16`)
|
|
159
|
-
|
|
164
|
+
- **Root (`15`, `16`)**: an independent thought, even if inspired by another intent.
|
|
165
|
+
Reserve `sources` for true created-from provenance (intents this was built out of). An
|
|
166
|
+
independent intent merely related to or inspired by another carries NO `sources`; record
|
|
167
|
+
the relation on the PREDECESSOR's `chain` (and mirror it as a `[[id]]` wikilink in
|
|
168
|
+
`## Links`).
|
|
160
169
|
- **Rule of thumb:** if the intent could exist without its parent, it's a root.
|
|
161
170
|
|
|
162
171
|
## INDEX.md
|
|
@@ -27,7 +27,15 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read the intent file, its `## Links`, and related code or docs
|
|
28
28
|
3. Research with the adaptive budget the enforcer set (simple 2-3, medium 5-8, complex 10-15 steps)
|
|
29
29
|
4. Produce (output handoff): an enriched `## Context` plus `### Decisions` with rationale
|
|
30
|
-
5. Log autonomous calls in `## Insights` with the `(autonomous)` marker, then report for gating
|
|
30
|
+
5. Log autonomous calls in `## Insights` with the `(autonomous)` marker, then report for gating (see `## Completion Report`)
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the brainstorming payload:
|
|
35
|
+
|
|
36
|
+
- Decisions recorded in `### Decisions`, each with its one-line rationale
|
|
37
|
+
- Context enriched: what was researched and the key findings
|
|
38
|
+
- Open questions resolved, and any deliberately left for the spec
|
|
31
39
|
|
|
32
40
|
## Constraints
|
|
33
41
|
|
|
@@ -28,6 +28,16 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
28
28
|
3. Edit project code (the gate is open now that plan and checklist exist)
|
|
29
29
|
4. Run the full suite, iterate to zero failures and zero errors
|
|
30
30
|
5. Produce (output handoff): the code changes, a checked-off `checklist.md`, and `## Insights`
|
|
31
|
+
6. Report for gating (see `## Completion Report`); the enforcer reviews the work at the final gate
|
|
32
|
+
|
|
33
|
+
## Completion Report
|
|
34
|
+
|
|
35
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the executor payload:
|
|
36
|
+
|
|
37
|
+
- Actions implemented this turn, mapped to checklist items checked off (checked / total)
|
|
38
|
+
- A summary of the code changed (files and the shape of the change)
|
|
39
|
+
- Test result: the full-suite command and its pass / fail counts
|
|
40
|
+
- Insights appended, with the `(autonomous)` marker
|
|
31
41
|
|
|
32
42
|
## Constraints
|
|
33
43
|
|
|
@@ -27,7 +27,17 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read `spec.md` and the plan/checklist templates; match their FORM
|
|
28
28
|
3. Write `plan.md`, the `actions/` directory, and `checklist.md` into the intent directory
|
|
29
29
|
4. Produce (output handoff): `plan.md` plus `actions/` plus `checklist.md`
|
|
30
|
-
5. Report for gating; the enforcer verifies plan and checklist exist before Exec is allowed
|
|
30
|
+
5. Report for gating (see `## Completion Report`); the enforcer verifies plan and checklist exist before Exec is allowed
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the planner payload, which EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR:
|
|
35
|
+
|
|
36
|
+
- The ordered actions, one line each: what the action does and how it is verified
|
|
37
|
+
- Decomposition rationale: why this order, and why the actions are independent
|
|
38
|
+
- Checklist coverage: the item count, and that every action plus suite-green is covered
|
|
39
|
+
|
|
40
|
+
The plan is an argument; the orchestrator gates on whether that argument is sound before any code is written, so make the report make that case.
|
|
31
41
|
|
|
32
42
|
## Constraints
|
|
33
43
|
|
|
@@ -27,7 +27,15 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read the spec template (`templates/spec.md`) and match its FORM exactly
|
|
28
28
|
3. Write `spec.md` into the intent directory, resolving the decisions into a coherent approach
|
|
29
29
|
4. Produce (output handoff): a complete `spec.md`
|
|
30
|
-
5. Report for gating; the enforcer checks the spec against its exit criteria before How begins
|
|
30
|
+
5. Report for gating (see `## Completion Report`); the enforcer checks the spec against its exit criteria before How begins
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the spec payload:
|
|
35
|
+
|
|
36
|
+
- The spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria)
|
|
37
|
+
- How the recorded decisions resolved into the chosen approach
|
|
38
|
+
- The acceptance-criteria count, so the planner knows the surface to cover
|
|
31
39
|
|
|
32
40
|
## Constraints
|
|
33
41
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# agent-report - deterministic filesystem-derived completion report (intent 74).
|
|
6
|
+
#
|
|
7
|
+
# The auto-mode report contract is decision-shaped (the spawn preamble's
|
|
8
|
+
# REPORT_CONTRACT plus the role prompts ask every dispatched agent to END with a
|
|
9
|
+
# structured completion report). Because child-agent honor is best-effort across
|
|
10
|
+
# harnesses (Tier B/C, docs/reference/harness-adapters.md), the contract is never a
|
|
11
|
+
# hard block. This script is the always-a-report fallback: when a dispatched agent
|
|
12
|
+
# returns no usable report, the enforcer runs it to synthesize one from the intent
|
|
13
|
+
# directory, so the handoff account always exists.
|
|
14
|
+
#
|
|
15
|
+
# Like scripts/spawn-preamble, it is a PURE function of the intent dir: no network,
|
|
16
|
+
# no randomness, no wall-clock reads. Two runs over the same on-disk state produce
|
|
17
|
+
# byte-identical output.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# agent-report <intent_dir> [--role ROLE]
|
|
21
|
+
#
|
|
22
|
+
# Exit codes: 0 (report emitted), 2 (usage).
|
|
23
|
+
|
|
24
|
+
require_relative "lib/bridge"
|
|
25
|
+
|
|
26
|
+
def parse_args(argv)
|
|
27
|
+
role = nil
|
|
28
|
+
positional = []
|
|
29
|
+
i = 0
|
|
30
|
+
while i < argv.length
|
|
31
|
+
case argv[i]
|
|
32
|
+
when "--role"
|
|
33
|
+
role = argv[i + 1]
|
|
34
|
+
i += 2
|
|
35
|
+
else
|
|
36
|
+
positional << argv[i]
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
[positional.first, role]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read the intent file frontmatter via the same path spawn-preamble uses, so the
|
|
44
|
+
# id/intent we report match what the rest of Plastic sees. Never raises.
|
|
45
|
+
def frontmatter_for(intent_dir)
|
|
46
|
+
ifile = Bridge.intent_file(intent_dir)
|
|
47
|
+
return {} unless File.exist?(ifile)
|
|
48
|
+
|
|
49
|
+
content = File.read(ifile)
|
|
50
|
+
return {} unless content.start_with?("---")
|
|
51
|
+
|
|
52
|
+
parts = content.split("---", 3)
|
|
53
|
+
return {} if parts.length < 3
|
|
54
|
+
|
|
55
|
+
require "yaml"
|
|
56
|
+
require "date"
|
|
57
|
+
require "time"
|
|
58
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}
|
|
59
|
+
rescue StandardError
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Current stage: prefer the last non-empty savepoint line (the ledger encodes the
|
|
64
|
+
# furthest-reached milestone), else the file-derived stage. Same logic as
|
|
65
|
+
# spawn-preamble's current_stage.
|
|
66
|
+
STAGE_LABELS = {
|
|
67
|
+
"what" => "What", "why" => "Why", "how" => "How",
|
|
68
|
+
"exec" => "Exec", "done" => "Done"
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
def current_stage(intent_dir)
|
|
72
|
+
ledger = File.join(intent_dir, Bridge::SAVEPOINT_FILE)
|
|
73
|
+
if File.exist?(ledger)
|
|
74
|
+
last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
|
|
75
|
+
return last if last
|
|
76
|
+
end
|
|
77
|
+
STAGE_LABELS.fetch(Bridge.derive_stage(intent_dir), Bridge.derive_stage(intent_dir))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# checklist.md checked/total: count GFM task-list items. Returns "n/a" when there
|
|
81
|
+
# is no checklist (a checklist is only meaningful from How onward).
|
|
82
|
+
def checklist_progress(intent_dir)
|
|
83
|
+
path = File.join(intent_dir, "checklist.md")
|
|
84
|
+
return "n/a" unless Bridge.stage_file_present?(path)
|
|
85
|
+
|
|
86
|
+
lines = File.readlines(path)
|
|
87
|
+
total = lines.count { |l| l =~ /^\s*- \[[ xX]\]/ }
|
|
88
|
+
checked = lines.count { |l| l =~ /^\s*- \[[xX]\]/ }
|
|
89
|
+
return "n/a" if total.zero?
|
|
90
|
+
|
|
91
|
+
"#{checked}/#{total}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The outcome line: first content line under outcome.md's "## Summary" (or the first
|
|
95
|
+
# non-heading content line), else a placeholder. Pure read of the intent dir.
|
|
96
|
+
def outcome_line(intent_dir)
|
|
97
|
+
path = File.join(intent_dir, "outcome.md")
|
|
98
|
+
return "(no outcome yet)" unless Bridge.stage_file_present?(path)
|
|
99
|
+
|
|
100
|
+
lines = File.readlines(path).map(&:rstrip)
|
|
101
|
+
start = lines.index { |l| l.strip.downcase == "## summary" }
|
|
102
|
+
scan = start ? lines[(start + 1)..] : lines
|
|
103
|
+
(scan || []).each do |l|
|
|
104
|
+
s = l.strip
|
|
105
|
+
next if s.empty? || s.start_with?("#") || s.start_with?("<!--")
|
|
106
|
+
return s
|
|
107
|
+
end
|
|
108
|
+
"(no outcome yet)"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
intent_dir_arg, role = parse_args(ARGV)
|
|
112
|
+
|
|
113
|
+
if intent_dir_arg.nil? || intent_dir_arg.empty?
|
|
114
|
+
warn "usage: agent-report <intent_dir> [--role ROLE]"
|
|
115
|
+
exit 2
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
119
|
+
|
|
120
|
+
fm = frontmatter_for(intent_dir)
|
|
121
|
+
id = fm["id"].to_s.strip
|
|
122
|
+
intent_name = fm["intent"].to_s.strip
|
|
123
|
+
id = "(unknown)" if id.empty?
|
|
124
|
+
intent_name = "(unknown)" if intent_name.empty?
|
|
125
|
+
|
|
126
|
+
stage = current_stage(intent_dir)
|
|
127
|
+
role_label = (role && !role.empty?) ? role : stage
|
|
128
|
+
artifacts = Bridge.has_files(intent_dir)
|
|
129
|
+
artifacts_str = artifacts.empty? ? "(none)" : artifacts.join(", ")
|
|
130
|
+
|
|
131
|
+
lines = []
|
|
132
|
+
lines << "=== Plastic agent report (synthesized) ==="
|
|
133
|
+
lines << "Intent: #{id} - #{intent_name}"
|
|
134
|
+
lines << "Stage: #{stage}"
|
|
135
|
+
lines << "Role: #{role_label}"
|
|
136
|
+
lines << "Status: synthesized (filesystem-derived; no agent-authored report)"
|
|
137
|
+
lines << "Artifacts present: #{artifacts_str}"
|
|
138
|
+
lines << "Checklist: #{checklist_progress(intent_dir)}"
|
|
139
|
+
lines << "Outcome: #{outcome_line(intent_dir)}"
|
|
140
|
+
lines << "=== end report ==="
|
|
141
|
+
|
|
142
|
+
puts lines.join("\n")
|
package/scripts/dashboard.rb
CHANGED
|
@@ -223,15 +223,17 @@ def effort_of(rec, type)
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# Value -> :high | :low (explicit frontmatter field wins).
|
|
226
|
-
# High is deliberately rare: an explicit stamp,
|
|
227
|
-
#
|
|
226
|
+
# High is deliberately rare: an explicit stamp, a human-authored root idea, or an intent
|
|
227
|
+
# that has SPAWNED follow-on work, i.e. a strategic theme the user owns. "Has spawned work"
|
|
228
|
+
# means a reciprocal (I1) edge: another intent lists this one in its `sources`, captured by
|
|
229
|
+
# `referenced`. A purely relational `chain` entry (D2, no reciprocal `sources`) does NOT
|
|
230
|
+
# count as spawned, so bare `chain` membership is not a high-value signal (intent 68).
|
|
228
231
|
def value_of(rec, referenced = {})
|
|
229
232
|
case rec[:value_field]
|
|
230
233
|
when "high" then return :high
|
|
231
234
|
when "low" then return :low
|
|
232
235
|
end
|
|
233
236
|
return :high if rec[:author] == "human" && root_intent?(rec[:id])
|
|
234
|
-
return :high unless rec[:chain].empty?
|
|
235
237
|
return :high if referenced[[rec[:scope], rec[:id]]]
|
|
236
238
|
:low
|
|
237
239
|
end
|
package/scripts/doctor.rb
CHANGED
|
@@ -473,9 +473,80 @@ class Doctor
|
|
|
473
473
|
)
|
|
474
474
|
end
|
|
475
475
|
|
|
476
|
+
# graph_invariants — cross-intent I1/I3/I4 checks (intent 68). I1/I3/I4 are
|
|
477
|
+
# defined within a single store's id space (bare ids resolve within the same
|
|
478
|
+
# store), so build the `nodes` map per scope and run validate_graph per scope.
|
|
479
|
+
# I2 asymmetry (a relational chain entry with no reciprocal sources) is NEVER
|
|
480
|
+
# flagged: validate_graph does not compute it.
|
|
481
|
+
checks.concat(graph_invariant_checks(intent_dirs))
|
|
482
|
+
|
|
483
|
+
checks
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Build a per-scope `nodes` map and surface IntentValidator.validate_graph
|
|
487
|
+
# findings as warn-level checks. Scope-aware (the caller already filtered
|
|
488
|
+
# `intent_dirs` by scope), so a `global` id is not falsely flagged as a dangler
|
|
489
|
+
# when only a `project:` store is loaded, and vice versa.
|
|
490
|
+
def graph_invariant_checks(intent_dirs)
|
|
491
|
+
nodes_by_scope = Hash.new { |h, k| h[k] = {} }
|
|
492
|
+
intent_dirs.each do |d|
|
|
493
|
+
md_path = File.join(d[:path], "#{d[:name]}.md")
|
|
494
|
+
next unless File.exist?(md_path)
|
|
495
|
+
|
|
496
|
+
fm = parse_frontmatter(md_path)
|
|
497
|
+
next unless fm.is_a?(Hash) && fm["id"]
|
|
498
|
+
|
|
499
|
+
nodes_by_scope[d[:scope]][fm["id"].to_s] = {
|
|
500
|
+
sources: Array(fm["sources"]).map(&:to_s),
|
|
501
|
+
chain: Array(fm["chain"]).map(&:to_s),
|
|
502
|
+
}
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
i1 = []
|
|
506
|
+
i3 = []
|
|
507
|
+
i4 = []
|
|
508
|
+
nodes_by_scope.each_value do |nodes|
|
|
509
|
+
findings = IntentValidator.validate_graph(nodes)
|
|
510
|
+
i1.concat(findings[:i1])
|
|
511
|
+
i3.concat(findings[:i3])
|
|
512
|
+
i4.concat(findings[:i4])
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
checks = []
|
|
516
|
+
checks << graph_finding_check(
|
|
517
|
+
"graph_i1_reciprocity", i1,
|
|
518
|
+
"Every sources edge has its reciprocal chain entry (I1)",
|
|
519
|
+
"Run new-intent / the rebuild so each source intent's chain backlinks the child"
|
|
520
|
+
)
|
|
521
|
+
checks << graph_finding_check(
|
|
522
|
+
"graph_i3_disjoint", i3,
|
|
523
|
+
"No intent lists the same id in both sources and chain (I3)",
|
|
524
|
+
"Remove the overlapping id from either sources or chain"
|
|
525
|
+
)
|
|
526
|
+
checks << graph_finding_check(
|
|
527
|
+
"graph_i4_danglers", i4,
|
|
528
|
+
"Every sources/chain id resolves to a real intent (I4)",
|
|
529
|
+
"Fix or remove the dangling id reference"
|
|
530
|
+
)
|
|
476
531
|
checks
|
|
477
532
|
end
|
|
478
533
|
|
|
534
|
+
# One graph check: pass when `findings` is empty, otherwise warn (never fail, so
|
|
535
|
+
# an existing store does not turn red on a graph finding). I1/I4 are auto-fixable.
|
|
536
|
+
def graph_finding_check(name, findings, pass_message, fix_hint)
|
|
537
|
+
if findings.empty?
|
|
538
|
+
check(category: "conventions", name: name, status: "pass", message: pass_message)
|
|
539
|
+
else
|
|
540
|
+
check(
|
|
541
|
+
category: "conventions", name: name, status: "warn",
|
|
542
|
+
message: "#{findings.size} #{name} violation(s)",
|
|
543
|
+
details: findings,
|
|
544
|
+
fixable: name != "graph_i3_disjoint",
|
|
545
|
+
fix_hint: fix_hint
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
479
550
|
# --- Check category 3: Agent registration ---
|
|
480
551
|
|
|
481
552
|
def check_agent_registration(agent_key)
|
|
@@ -153,4 +153,83 @@ module IntentValidator
|
|
|
153
153
|
content = File.exist?(md_path) ? File.read(md_path) : nil
|
|
154
154
|
validate_content(content)
|
|
155
155
|
end
|
|
156
|
+
|
|
157
|
+
# PURE: cross-intent graph-shape invariants (intent 68). These need visibility
|
|
158
|
+
# over the whole intent set, so they live apart from the single-file born-complete
|
|
159
|
+
# helpers above (which must not drift). No file IO: the caller builds `nodes`.
|
|
160
|
+
#
|
|
161
|
+
# `nodes` is a Hash { id(String) => { sources: [ids], chain: [ids] } } for every
|
|
162
|
+
# intent in ONE store's id space. Returns { i1: [...], i3: [...], i4: [...] },
|
|
163
|
+
# each an array of human-readable finding strings.
|
|
164
|
+
#
|
|
165
|
+
# I2 (no false symmetry) is INTENTIONALLY not computed: a relational `chain` entry
|
|
166
|
+
# with no reciprocal `sources` is valid and must never be flagged.
|
|
167
|
+
def validate_graph(nodes)
|
|
168
|
+
nodes = normalize_nodes(nodes)
|
|
169
|
+
{ i1: graph_i1(nodes), i3: graph_i3(nodes), i4: graph_i4(nodes) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Coerce node arrays to deduped String id lists; tolerate missing keys.
|
|
173
|
+
def normalize_nodes(nodes)
|
|
174
|
+
return {} unless nodes.is_a?(Hash)
|
|
175
|
+
|
|
176
|
+
nodes.each_with_object({}) do |(id, edges), acc|
|
|
177
|
+
edges = {} unless edges.is_a?(Hash)
|
|
178
|
+
acc[id.to_s] = {
|
|
179
|
+
sources: Array(edges[:sources] || edges["sources"]).map(&:to_s).uniq,
|
|
180
|
+
chain: Array(edges[:chain] || edges["chain"]).map(&:to_s).uniq,
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# An id is a cross-store reference (out of this store's scope) when it carries a
|
|
186
|
+
# `<store>:` prefix, mirroring how `valid_id?` accepts the prefix. Such refs are
|
|
187
|
+
# resolved outside this node set, so they are never danglers here.
|
|
188
|
+
def cross_store_ref?(id)
|
|
189
|
+
id.to_s.include?(":")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# I1 (formative reciprocity): for every B and every `s` in B.sources that resolves
|
|
193
|
+
# in this store, B must appear in s.chain. A `s` that does not resolve is an I4
|
|
194
|
+
# dangler, not an I1 violation, so it is skipped here.
|
|
195
|
+
def graph_i1(nodes)
|
|
196
|
+
findings = []
|
|
197
|
+
nodes.each do |b_id, edges|
|
|
198
|
+
edges[:sources].each do |s|
|
|
199
|
+
next if cross_store_ref?(s)
|
|
200
|
+
next unless nodes.key?(s)
|
|
201
|
+
|
|
202
|
+
findings << "#{b_id}.sources lists #{s} but #{s}.chain is missing #{b_id}" unless nodes[s][:chain].include?(b_id)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
findings
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# I3 (per-node disjoint): X.sources and X.chain must not overlap.
|
|
209
|
+
def graph_i3(nodes)
|
|
210
|
+
findings = []
|
|
211
|
+
nodes.each do |x_id, edges|
|
|
212
|
+
(edges[:sources] & edges[:chain]).each do |overlap|
|
|
213
|
+
findings << "#{x_id} lists #{overlap} in BOTH sources and chain"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
findings
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# I4 (no danglers): every bare (same-store) id in any sources/chain must resolve
|
|
220
|
+
# to a node. Cross-store `<store>:<id>` refs resolve elsewhere and are not flagged.
|
|
221
|
+
def graph_i4(nodes)
|
|
222
|
+
findings = []
|
|
223
|
+
nodes.each do |id, edges|
|
|
224
|
+
%i[sources chain].each do |field|
|
|
225
|
+
edges[field].each do |ref|
|
|
226
|
+
next if cross_store_ref?(ref)
|
|
227
|
+
next if nodes.key?(ref)
|
|
228
|
+
|
|
229
|
+
findings << "#{id}.#{field} references #{ref} which resolves to no intent"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
findings
|
|
234
|
+
end
|
|
156
235
|
end
|
package/scripts/new-intent
CHANGED
|
@@ -73,6 +73,39 @@ def render_tokens(text, tokens)
|
|
|
73
73
|
tokens.reduce(text) { |acc, (k, v)| acc.gsub("{{#{k}}}", v.to_s) }
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Add an id to a source intent's frontmatter `chain` array, idempotently (I1
|
|
77
|
+
# reciprocity: `child in parent.sources` => `parent.chain` gains `child`). A
|
|
78
|
+
# targeted edit of the `chain:` line only; the body (including `## Links`) is
|
|
79
|
+
# preserved byte-for-byte and no other frontmatter key is touched, so the file
|
|
80
|
+
# stays born-complete. Renders the array in flow style (`["a", "b"]`) to match
|
|
81
|
+
# templates/intent.md. No-op when the id is already present.
|
|
82
|
+
def add_to_chain(file_path, new_id)
|
|
83
|
+
return unless File.exist?(file_path)
|
|
84
|
+
content = File.read(file_path)
|
|
85
|
+
return unless content.start_with?("---")
|
|
86
|
+
|
|
87
|
+
parts = content.split("---", 3)
|
|
88
|
+
return unless parts.length >= 3
|
|
89
|
+
|
|
90
|
+
fm = parts[1]
|
|
91
|
+
chain_line = fm.lines.find { |l| l.match?(/\A\s*chain\s*:/) }
|
|
92
|
+
return unless chain_line
|
|
93
|
+
|
|
94
|
+
existing = fm.match(/\bchain\s*:\s*\[(.*?)\]/m)
|
|
95
|
+
ids =
|
|
96
|
+
if existing
|
|
97
|
+
existing[1].scan(/"([^"]*)"|'([^']*)'/).flatten.compact
|
|
98
|
+
else
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
return if ids.include?(new_id)
|
|
102
|
+
|
|
103
|
+
ids << new_id
|
|
104
|
+
rendered = "chain: [#{ids.map { |i| "\"#{i}\"" }.join(", ")}]"
|
|
105
|
+
new_fm = fm.sub(/^\s*chain\s*:.*$/, rendered)
|
|
106
|
+
File.write(file_path, ["", new_fm, parts[2]].join("---"))
|
|
107
|
+
end
|
|
108
|
+
|
|
76
109
|
# Append a wikilink line under the file's `## Links` section, idempotently.
|
|
77
110
|
def append_link(file_path, link_line)
|
|
78
111
|
return unless File.exist?(file_path)
|
|
@@ -140,7 +173,23 @@ def main(argv)
|
|
|
140
173
|
intent_file = File.join(intent_dir, "#{id}--#{slug}.md")
|
|
141
174
|
File.write(intent_file, intent_body)
|
|
142
175
|
|
|
143
|
-
#
|
|
176
|
+
# 4a. I1 reciprocity: write the child's id into EACH source intent's frontmatter
|
|
177
|
+
# `chain` (the formative-reciprocity backlink), for BOTH the `--parent` and the
|
|
178
|
+
# `--sources` path. `sources` is the redundant-explicit set from step 3 (it already
|
|
179
|
+
# folds in `--parent`). Scope boundary: 68 fixes ONLY the frontmatter `chain`
|
|
180
|
+
# backlink; the `## Links` wikilink projection for the `--sources` path is intent 72.
|
|
181
|
+
sources.each do |src_id|
|
|
182
|
+
next if src_id.nil? || src_id.empty?
|
|
183
|
+
|
|
184
|
+
src_dir = Dir.glob(File.join(store, "#{src_id}--*")).find { |d| File.directory?(d) }
|
|
185
|
+
next unless src_dir
|
|
186
|
+
|
|
187
|
+
src_file = File.join(src_dir, "#{File.basename(src_dir)}.md")
|
|
188
|
+
add_to_chain(src_file, id)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# 4b. Reciprocal `## Links` wikilinks: forward link to parent + back-reference in
|
|
192
|
+
# the parent (kept parent-only; the `--sources` path's wikilink is intent 72's scope).
|
|
144
193
|
if opts[:parent] && !opts[:parent].empty?
|
|
145
194
|
parent_id = opts[:parent]
|
|
146
195
|
append_link(intent_file, "- [[#{parent_id}]]")
|
package/scripts/spawn-preamble
CHANGED
|
@@ -29,7 +29,22 @@ require_relative "lib/bridge"
|
|
|
29
29
|
HONOR_INSTRUCTION =
|
|
30
30
|
"You are operating inside Plastic. Use it as your operating scaffold. " \
|
|
31
31
|
"Emit VALID lifecycle artifacts; do not hallucinate intents or stages. " \
|
|
32
|
-
"Your output is
|
|
32
|
+
"Your primary output is valid lifecycle artifacts; you close with a structured report about them."
|
|
33
|
+
|
|
34
|
+
# Verbatim completion-report contract (intent 74). Kept as one constant so the
|
|
35
|
+
# contract doc (skills/auto/references/agent-report-contract.md), the role prompts,
|
|
36
|
+
# and the test assert against the exact same string. SINGLE SOURCE OF TRUTH for the
|
|
37
|
+
# report wording: the work output is lifecycle artifacts, the final message is a
|
|
38
|
+
# structured report about them. Both are required and they do not contradict.
|
|
39
|
+
REPORT_CONTRACT =
|
|
40
|
+
"Before you finish, END your turn with a structured completion report as your " \
|
|
41
|
+
"FINAL MESSAGE (your return value), not a side-channel file. Do not go idle or " \
|
|
42
|
+
"finish silently. The report carries a common envelope: role, intent id, stage, " \
|
|
43
|
+
"status (delivered or blocked), artifacts written, verification or tests run, " \
|
|
44
|
+
"checklist deltas, deviations from spec, and blockers or handoff notes; plus a " \
|
|
45
|
+
"role-specific payload that fulfils your place in the What, Why, How, Exec cycle " \
|
|
46
|
+
"(for example the planner explains the plan back to the orchestrator). See " \
|
|
47
|
+
"skills/auto/references/agent-report-contract.md for the per-role format."
|
|
33
48
|
|
|
34
49
|
def parse_args(argv)
|
|
35
50
|
role = nil
|
|
@@ -116,6 +131,8 @@ lines << "Current stage: #{stage}"
|
|
|
116
131
|
lines << "Cycle step / role: #{cycle}"
|
|
117
132
|
lines << ""
|
|
118
133
|
lines << HONOR_INSTRUCTION
|
|
134
|
+
lines << ""
|
|
135
|
+
lines << REPORT_CONTRACT
|
|
119
136
|
lines << "=== end preamble ==="
|
|
120
137
|
|
|
121
138
|
puts lines.join("\n")
|
package/skills/auto/SKILL.md
CHANGED
|
@@ -68,6 +68,8 @@ Dispatch rule: sequential, one specialist per stage on one branch (the deliverab
|
|
|
68
68
|
|
|
69
69
|
Spawn preamble (live-state injection): before dispatching any specialist, run `scripts/spawn-preamble <intent_dir> --role <role>` and PREPEND its output to that specialist's prompt. The preamble is a deterministic, filesystem-only snapshot of the active intent (id, intent line, current stage) plus the honoring instruction, so every spawned agent boots with accurate live state instead of guessing. This is the authoritative L2 mechanism for harnesses whose sub-agents do not inherit a top-level session event (see `docs/reference/harness-adapters.md`).
|
|
70
70
|
|
|
71
|
+
Completion report (require-then-synthesize): every dispatched specialist MUST end with a structured completion report as its final message. The preamble's `REPORT_CONTRACT` injects this and the role prompts carry the per-role format (see `references/agent-report-contract.md`). Because child-agent honor is best-effort across harnesses, this is decision-shaping, not a hard block. When a specialist returns no usable report (it went idle, emitted only a bare ping, or its message was lost to a mid-run interjection), run `scripts/agent-report <intent_dir> --role <role>` to synthesize a deterministic filesystem-derived report so the handoff account always exists. Use the agent-authored report when present, the synthesized one otherwise.
|
|
72
|
+
|
|
71
73
|
Final-gate review: dispatch an independent reviewer subagent at the final gate only, not as a standing role.
|
|
72
74
|
|
|
73
75
|
Headless manual gate: when running headless or in the background, enforce gates manually and do not rely on hooks, because `CLAUDE_SESSION_ID` may be unset (this ties to the arm-gate fallback above).
|
|
@@ -61,6 +61,26 @@ and not hallucinate intents or stages. This is the standard L2 live-state mechan
|
|
|
61
61
|
for harnesses whose spawned sub-agents do not inherit the top-level session event. See
|
|
62
62
|
`docs/reference/harness-adapters.md` for how it slots into the per-harness contract.
|
|
63
63
|
|
|
64
|
+
### Completion Reports
|
|
65
|
+
|
|
66
|
+
Every dispatched specialist ends its turn with a structured completion report as its final
|
|
67
|
+
message (its return value), so the agent that did the work is the one that accounts for it. The
|
|
68
|
+
report carries a common envelope plus a role-specific payload that fulfils the agent's place in
|
|
69
|
+
the cycle; the planner explains the plan back to the orchestrator, the executor reports what was
|
|
70
|
+
built and the test result, and so on. The format lives in `references/agent-report-contract.md`,
|
|
71
|
+
and the verbatim instruction is injected once via the spawn preamble's `REPORT_CONTRACT`
|
|
72
|
+
constant, which the role prompts reproduce.
|
|
73
|
+
|
|
74
|
+
Enforcement is require-report then synthesize-fallback. The preamble and prompts make the report
|
|
75
|
+
mandatory (decision-shaping), but child-agent honor is best-effort across harnesses (Tier B/C),
|
|
76
|
+
so it is never a hard block. When a specialist returns no usable report, the enforcer runs
|
|
77
|
+
`scripts/agent-report <intent_dir> --role <role>`, a pure function of the intent dir (no network,
|
|
78
|
+
clock, or randomness, mirroring `spawn-preamble`) that emits a filesystem-derived report from the
|
|
79
|
+
savepoint, the artifacts present, the checklist checked/total, and the outcome line. A handoff
|
|
80
|
+
account therefore always exists: agent-authored when present, deterministically reconstructed
|
|
81
|
+
otherwise. This structures the finish notification only; in-flight observations stay in
|
|
82
|
+
`## Insights`, no progress chatter is added.
|
|
83
|
+
|
|
64
84
|
### Gate Ownership
|
|
65
85
|
|
|
66
86
|
The enforcer arms and verifies the lifecycle gate, then gates every stage transition.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Agent Completion Report Contract
|
|
2
|
+
|
|
3
|
+
Every agent dispatched by the auto-mode enforcer MUST end its turn with a structured
|
|
4
|
+
completion report. This doc defines that report: one common envelope plus a per-role payload.
|
|
5
|
+
It is the format the `REPORT_CONTRACT` constant in `scripts/spawn-preamble` points at, the
|
|
6
|
+
role prompts (`agents/plastic-*.md`) reproduce, and the deterministic fallback
|
|
7
|
+
(`scripts/agent-report`) approximates. Keep all four in agreement; the constant in
|
|
8
|
+
`scripts/spawn-preamble` is the single source of truth for the injected wording.
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
The report is the agent's FINAL MESSAGE (its return value), not a side-channel file. Every
|
|
13
|
+
harness hands a spawned agent's final text back to the dispatcher, so the final message is the
|
|
14
|
+
one carrier that works everywhere (decision D1). The report structures the FINISH notification
|
|
15
|
+
only. In-flight observations still go in `## Insights`; the report does not add progress chatter
|
|
16
|
+
(decision D5). An agent that finishes correct artifacts but goes idle without a report has not
|
|
17
|
+
completed its handoff: the agent that did the work is the cheapest, most accurate source of the
|
|
18
|
+
account.
|
|
19
|
+
|
|
20
|
+
## Common envelope
|
|
21
|
+
|
|
22
|
+
Every role report, whatever the stage, carries these fields:
|
|
23
|
+
|
|
24
|
+
- **Role**: which specialist produced this (brainstorming, spec, planner, executor, reviewer).
|
|
25
|
+
- **Intent id and stage**: the active intent id and the cycle stage just completed.
|
|
26
|
+
- **Status**: `delivered` or `blocked`.
|
|
27
|
+
- **Artifacts written**: the files produced or changed (store paths, and project paths for the
|
|
28
|
+
executor).
|
|
29
|
+
- **Verification / tests run**: the command run and its result, or `n/a` for stages that write
|
|
30
|
+
no code.
|
|
31
|
+
- **Checklist deltas**: which checklist items this turn checked off (executor), or `n/a`.
|
|
32
|
+
- **Deviations from spec**: anything done differently from the spec or plan, and why, or `none`.
|
|
33
|
+
- **Blockers / handoff notes**: what the next stage must watch for, or `none`.
|
|
34
|
+
|
|
35
|
+
## Per-role payload
|
|
36
|
+
|
|
37
|
+
Each role appends a payload that fulfils its place in the What, Why, How, Exec cycle (decision
|
|
38
|
+
D2). The payload is what makes the report useful to the orchestrator beyond the envelope.
|
|
39
|
+
|
|
40
|
+
### brainstorming (Why exploration)
|
|
41
|
+
- Decisions recorded in `### Decisions`, each with its one-line rationale.
|
|
42
|
+
- Context enriched: what was researched and the key findings.
|
|
43
|
+
- Open questions resolved, and any deliberately left for the spec.
|
|
44
|
+
|
|
45
|
+
### spec-specialist (Why to How boundary)
|
|
46
|
+
- Spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria).
|
|
47
|
+
- How the recorded decisions resolved into the chosen approach.
|
|
48
|
+
- Acceptance-criteria count, so the planner knows the surface to cover.
|
|
49
|
+
|
|
50
|
+
### planner (How): worked exemplar
|
|
51
|
+
The planner report EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR. It carries:
|
|
52
|
+
- The ordered actions, one line each: what the action does and how it is verified.
|
|
53
|
+
- Decomposition rationale: why this order, and why the actions are independent.
|
|
54
|
+
- Checklist coverage: item count and that every action plus suite-green is covered.
|
|
55
|
+
This is the exemplar because the plan is an argument, and the orchestrator gates on whether that
|
|
56
|
+
argument is sound before any code is written.
|
|
57
|
+
|
|
58
|
+
### executor (Exec)
|
|
59
|
+
- Actions implemented this turn, mapped to checklist items checked off (checked / total).
|
|
60
|
+
- A summary of the code changed (files and the shape of the change).
|
|
61
|
+
- Test result: the full-suite command and its pass / fail counts.
|
|
62
|
+
- Insights appended, with the `(autonomous)` marker.
|
|
63
|
+
|
|
64
|
+
### final reviewer (final gate)
|
|
65
|
+
- Verdict: `pass` or `blockers found`.
|
|
66
|
+
- Each acceptance criterion checked, with the evidence that confirms or refutes it.
|
|
67
|
+
- Gaps or risks found, ranked, with a recommended disposition.
|
|
68
|
+
|
|
69
|
+
## Fallback: always a report
|
|
70
|
+
|
|
71
|
+
Decision-shaping (the preamble plus these prompts) makes the report mandatory, but child-agent
|
|
72
|
+
honor is best-effort across harnesses (Tier B/C in `docs/reference/harness-adapters.md`), so the
|
|
73
|
+
contract is never a hard block (decision D3). When a dispatched agent returns no usable report
|
|
74
|
+
(it went idle, emitted only a bare ping, or its message was lost to a mid-run interjection), the
|
|
75
|
+
enforcer synthesizes one:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
scripts/agent-report <intent_dir> --role <role>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`scripts/agent-report` is a pure function of the intent directory (no network, clock, or
|
|
82
|
+
randomness, mirroring `scripts/spawn-preamble`): it reads the current stage from the savepoint
|
|
83
|
+
ledger, the lifecycle artifacts present, the checklist checked / total, and the `## Outcome`
|
|
84
|
+
line, and emits a filesystem-derived report labelled `synthesized`. So a handoff account always
|
|
85
|
+
exists: authored by the agent when possible, reconstructed deterministically when not. This
|
|
86
|
+
formalizes the by-hand reconstruction the orchestrator did while delivering intent 68.
|
|
@@ -30,7 +30,7 @@ description: Use when new work begins, the user expresses a new goal, says "new
|
|
|
30
30
|
|
|
31
31
|
When creating a tactical intent in a project store:
|
|
32
32
|
- Read the project's `AGENTS.md` for project context and decisions
|
|
33
|
-
- Link back to the project's governing intent (from `projects.yml` `parent` field) via `sources`
|
|
33
|
+
- Link back to the project's governing intent (from `projects.yml` `parent` field) via `sources` (the project genuinely is formed from its founding intent, a true formative edge, reciprocated on the founding intent's `chain`)
|
|
34
34
|
- Add `[[global:<parent_ID>]]` backlink in `## Links`
|
|
35
35
|
- The intent's Folgezettel ID is scoped to the project store (run `folgezettel-id` against the project's store at `~/.plastic/projects/{slug}/store/`)
|
|
36
36
|
|
|
@@ -48,10 +48,17 @@ Having a "parent" in mind does NOT automatically mean branch. Choose by meaning:
|
|
|
48
48
|
|
|
49
49
|
- **Branch (`14a`, `14b`)**: a sub-task, refinement, or direct continuation. It only
|
|
50
50
|
makes sense as part of the parent's work. Pass `--parent <parent_id>`.
|
|
51
|
-
- **Root (`15`, `16`)**: an independent thought
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
- **Root (`15`, `16`)**: an independent thought. Two cases, decided by ORIGIN:
|
|
52
|
+
- **Created from another intent** (it emerged from that intent's lifecycle): make it a
|
|
53
|
+
root and set `--sources <ascendant_id>`. `sources` is reserved for true created-from /
|
|
54
|
+
direct-ascendant provenance (D1).
|
|
55
|
+
- **Merely related to / inspired by another intent** (it did NOT come out of that
|
|
56
|
+
intent's lifecycle): carry NO `--sources`. Record the relation on the PREDECESSOR's
|
|
57
|
+
`chain` instead, and mirror it as a `[[id]]` wikilink in `## Links` (the
|
|
58
|
+
related-but-not-spawned rule).
|
|
59
|
+
- **Rule of thumb:** if the intent could exist without its parent, make it a root; only set
|
|
60
|
+
`--sources` when it was genuinely created from / emerged from that intent's lifecycle.
|
|
61
|
+
Topic similarity alone is not a `sources` edge.
|
|
55
62
|
|
|
56
63
|
### 3. Determine Intent Properties
|
|
57
64
|
|
|
@@ -59,11 +66,16 @@ Ask or infer from context:
|
|
|
59
66
|
- **intent**: one-line description
|
|
60
67
|
- **slug**: short hyphenated handle for the directory name
|
|
61
68
|
- **author**: `human` | `claude-code` | other agent name
|
|
62
|
-
- **sources**:
|
|
63
|
-
|
|
69
|
+
- **sources**: the direct ascendant(s) this intent was created from / emerged from the
|
|
70
|
+
lifecycle of (formation, not topic similarity), e.g., `4a1`. For a project intent,
|
|
71
|
+
include the governing intent's id. A branch's structural parent is ALSO recorded in
|
|
72
|
+
`sources` (the ID carries it for the human/paper tree, `sources` carries it for
|
|
73
|
+
software), which `new-intent` does automatically (see `new-intent:126`).
|
|
64
74
|
- **tags**: freeform list (use `project-<name>` for project membership)
|
|
65
75
|
|
|
66
|
-
`chain`
|
|
76
|
+
`chain` carries what this intent spawns AND related-but-not-spawned successors it leads to;
|
|
77
|
+
it starts empty and is populated later. See
|
|
78
|
+
`docs/concepts/how-plastic-sources-and-chains-intents.md` for the full model.
|
|
67
79
|
Place the intent in `## Active` or `## Future` in INDEX.md (status is
|
|
68
80
|
convention-derived, not a frontmatter field).
|
|
69
81
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "plastic-creating-intent",
|
|
3
|
+
"notes": "Intent 68. Scope: output-quality for the sources-vs-chain construction rules (D1/D2). Asserts the related-but-not-spawned case produces NO sources plus a predecessor chain link and a ## Links mirror, contrasted with the created-from case (true ascendant -> --sources set, reciprocal chain). The machine-checkable half lives in test/new_intent_test.rb (test_sources_path_gets_child_in_chain_frontmatter); this file documents the agent-facing scenario for skill evaluation and is NOT run by bin/test.",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"scope": "behavior",
|
|
8
|
+
"set": "train",
|
|
9
|
+
"prompt": "Create an intent for adding a retry policy to the uploader. It's related to intent 41 (the upload pipeline work) but it's independent: it did not come out of intent 41's lifecycle.",
|
|
10
|
+
"expected_output": "A new root intent is created with EMPTY sources (it was not created from 41). The relation is recorded on the PREDECESSOR: intent 41 gains the new intent's id in its frontmatter chain, and a [[<new-id>]] wikilink is added to intent 41's ## Links. The new intent is NOT given 41 in --sources (the related-but-not-spawned rule). No false symmetry: 41 keeps the new id on chain with no reciprocal sources.",
|
|
11
|
+
"files": [],
|
|
12
|
+
"assertions": [
|
|
13
|
+
{
|
|
14
|
+
"type": "code",
|
|
15
|
+
"check": "new intent sources is empty",
|
|
16
|
+
"observed": "sources: []",
|
|
17
|
+
"result": "pass"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "code",
|
|
21
|
+
"check": "predecessor 41 chain includes new id",
|
|
22
|
+
"observed": "41.chain includes <new-id>",
|
|
23
|
+
"result": "pass"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "code",
|
|
27
|
+
"check": "predecessor 41 ## Links has [[<new-id>]] mirror",
|
|
28
|
+
"observed": "[[<new-id>]] present in 41 ## Links",
|
|
29
|
+
"result": "pass"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 2,
|
|
35
|
+
"scope": "behavior",
|
|
36
|
+
"set": "validation",
|
|
37
|
+
"prompt": "Create an intent that is the direct continuation of intent 41: it emerged from intent 41's lifecycle and could not exist without it.",
|
|
38
|
+
"expected_output": "Because the new intent was genuinely CREATED FROM 41 (D1), it carries 41 in --sources (or branches from 41, which folds 41 into sources via the redundant-explicit rule). The reciprocal I1 backlink lands: intent 41's frontmatter chain gains the new intent's id. This is the created-from case, contrasted with the related-but-not-spawned case in eval 1.",
|
|
39
|
+
"files": [],
|
|
40
|
+
"assertions": [
|
|
41
|
+
{
|
|
42
|
+
"type": "code",
|
|
43
|
+
"check": "new intent sources includes 41",
|
|
44
|
+
"observed": "sources includes 41",
|
|
45
|
+
"result": "pass"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "code",
|
|
49
|
+
"check": "predecessor 41 chain includes new id (I1 reciprocity)",
|
|
50
|
+
"observed": "41.chain includes <new-id>",
|
|
51
|
+
"result": "pass"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -36,13 +36,16 @@ Never modified, only appended.
|
|
|
36
36
|
|
|
37
37
|
Tracks: stage transitions, decisions, shifts, blocks, cancellations, material for future intents.
|
|
38
38
|
This is how execution is tracked. When this intent completes, Insights
|
|
39
|
-
is where to look for what comes next. New intents spawned from
|
|
40
|
-
appear in the `chain`
|
|
39
|
+
is where to look for what comes next. New intents spawned from this one,
|
|
40
|
+
plus related-but-not-spawned successors it leads to, appear in the `chain`
|
|
41
|
+
field.
|
|
41
42
|
|
|
42
43
|
## `## Links`
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
The human-readable projection of the local knowledge graph: all `sources`
|
|
46
|
+
first (top, named), then all `chain` (named), as `[[id]]` wikilinks plus a
|
|
47
|
+
short label. Counterpart to the frontmatter `sources` / `chain` edges, for
|
|
48
|
+
Obsidian graph navigation.
|
|
46
49
|
|
|
47
50
|
## Conventions — Filesystem as Schema
|
|
48
51
|
|
|
@@ -21,13 +21,13 @@ Explicit wikilinks in the `## Links` section. Bidirectional — add to both inte
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### 2. Sources (Backward)
|
|
24
|
-
The `sources` array in frontmatter.
|
|
24
|
+
The `sources` array in frontmatter. The direct ascendant(s) this intent was created from / emerged from the lifecycle of (formation, not topic similarity), backward links to the work it was built out of:
|
|
25
25
|
```yaml
|
|
26
26
|
sources: ["1a", "1a2"]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 3. Chain (Forward)
|
|
30
|
-
The `chain` array in frontmatter. What this intent spawned
|
|
30
|
+
The `chain` array in frontmatter. What this intent spawned AND related-but-not-spawned successors it leads to, forward links to children, follow-on, and related work:
|
|
31
31
|
```yaml
|
|
32
32
|
chain: ["1b1", "1b2"]
|
|
33
33
|
```
|
|
@@ -55,16 +55,21 @@ done
|
|
|
55
55
|
|
|
56
56
|
### 2. Choose Connection Type
|
|
57
57
|
Ask the user which type of connection:
|
|
58
|
-
- **source
|
|
59
|
-
- **cross-reference
|
|
58
|
+
- **source**: "this was CREATED FROM that" (D1). The reciprocal update is one-directional (I1): add the ascendant id to this intent's `sources[]` AND add this intent's id to the ascendant's `chain[]`. A merely-related (not-created-from) connection is NOT a source: record it on the predecessor's `chain[]` only, plus a `## Links` wikilink, with NO `sources` (the related-but-not-spawned rule).
|
|
59
|
+
- **cross-reference**: "these are related" (add wikilink in `## Links` of both intents)
|
|
60
60
|
|
|
61
61
|
### 3. Apply Connection
|
|
62
62
|
|
|
63
|
-
**For sources:**
|
|
64
|
-
Update frontmatter arrays on both intents:
|
|
63
|
+
**For sources (a true created-from edge only):**
|
|
64
|
+
Update frontmatter arrays on both intents (I1, two-sided):
|
|
65
65
|
- Add the parent's ID to the child's `sources` array
|
|
66
66
|
- Add the child's ID to the parent's `chain` array
|
|
67
67
|
|
|
68
|
+
For the merely-related case, only the predecessor's `chain` (and both sides' `## Links`)
|
|
69
|
+
get the link, never `sources`. `chain` is NOT strictly the reverse of `sources` (I2):
|
|
70
|
+
relational `chain` entries are valid and must never be "corrected" by adding a reciprocal
|
|
71
|
+
`sources`.
|
|
72
|
+
|
|
68
73
|
**For cross-references:**
|
|
69
74
|
Add a wikilink in the `## Links` section of **both** intents (bidirectional).
|
|
70
75
|
|
|
@@ -20,9 +20,14 @@ IDs encode lineage using Luhmann's alternating convention:
|
|
|
20
20
|
|
|
21
21
|
## Knowledge Graph
|
|
22
22
|
|
|
23
|
-
`sources`
|
|
24
|
-
- `sources` =
|
|
25
|
-
|
|
23
|
+
`sources` and `chain` form the directed knowledge graph:
|
|
24
|
+
- `sources` = the direct ascendant(s) this intent was created from / emerged from the
|
|
25
|
+
lifecycle of (formation, not topic similarity); a DAG (acyclic), strong must-load context.
|
|
26
|
+
- `chain` = forward continuations AND related-but-not-spawned successors it leads to; a
|
|
27
|
+
directed graph that may cycle, lighter contributory context.
|
|
28
|
+
- Reciprocity is one-directional: every `sources` edge has a reciprocal `chain` entry (I1),
|
|
29
|
+
but `chain` may carry relational entries with no reciprocal `sources` (I2), so the graph is
|
|
30
|
+
NOT strictly double-linked.
|
|
26
31
|
|
|
27
32
|
## Dual-Mode
|
|
28
33
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
## Three Connection Types (Ranked)
|
|
10
10
|
|
|
11
11
|
1. **Direct links** (strongest) — wikilinks in `## Links` section
|
|
12
|
-
2. **Sources/Chain** (knowledge graph)
|
|
12
|
+
2. **Sources/Chain** (knowledge graph): `sources` = direct ascendants this was created from (formation, acyclic, must-load); `chain` = forward continuations and related successors (may cycle, lighter context). See `docs/concepts/how-plastic-sources-and-chains-intents.md` for the full model.
|
|
13
13
|
3. **Tags** (weakest) — shared tags, `project-<name>` for project membership
|
|
14
14
|
|
|
15
15
|
## When to Create a Cluster
|