okstra 0.171.0 → 0.172.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 (46) hide show
  1. package/docs/architecture.md +13 -0
  2. package/docs/cli.md +4 -2
  3. package/docs/for-ai/skills/okstra-user-response.md +2 -2
  4. package/docs/project-structure-overview.md +3 -1
  5. package/package.json +1 -1
  6. package/runtime/BUILD.json +2 -2
  7. package/runtime/prompts/launch.template.md +4 -0
  8. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  9. package/runtime/prompts/lead/okstra-lead-contract.md +36 -12
  10. package/runtime/prompts/lead/plan-body-verification.md +22 -11
  11. package/runtime/prompts/lead/report-writer.md +11 -10
  12. package/runtime/prompts/lead/team-contract.md +2 -0
  13. package/runtime/prompts/profiles/_clarification-recommendation.md +3 -1
  14. package/runtime/prompts/profiles/_common-contract.md +2 -1
  15. package/runtime/prompts/profiles/implementation-planning.md +8 -1
  16. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -1
  17. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +1 -1
  18. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
  19. package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
  20. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
  21. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +1 -1
  22. package/runtime/python/okstra_ctl/agent_activity.py +306 -0
  23. package/runtime/python/okstra_ctl/clarification_items.py +37 -20
  24. package/runtime/python/okstra_ctl/lead_events.py +47 -4
  25. package/runtime/python/okstra_ctl/render.py +11 -3
  26. package/runtime/python/okstra_ctl/report_finalize.py +51 -14
  27. package/runtime/python/okstra_ctl/report_html/common.py +5 -3
  28. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +17 -1
  29. package/runtime/python/okstra_ctl/report_translation.py +14 -0
  30. package/runtime/python/okstra_ctl/worker_audit_ledger.py +150 -0
  31. package/runtime/schemas/final-report-v2.0.schema.json +189 -0
  32. package/runtime/skills/okstra-user-response/SKILL.md +2 -2
  33. package/runtime/templates/reports/final-report-v2.template.md +8 -0
  34. package/runtime/templates/reports/html/assets/base.css +7 -0
  35. package/runtime/templates/reports/html/i18n/en.json +6 -1
  36. package/runtime/templates/reports/html/i18n/ko.json +6 -1
  37. package/runtime/templates/reports/html/macros/forms.html +21 -2
  38. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +25 -0
  39. package/runtime/templates/reports/i18n/en.json +4 -0
  40. package/runtime/templates/reports/report.js +26 -17
  41. package/runtime/templates/reports/user-response.template.md +3 -1
  42. package/runtime/templates/worker-prompt-preamble.md +8 -0
  43. package/runtime/validators/validate-run.py +989 -29
  44. package/runtime/validators/validate_session_conformance.py +523 -35
  45. package/src/cli-registry.mjs +7 -0
  46. package/src/commands/report/agent-activity.mjs +21 -0
@@ -108,6 +108,13 @@ textarea, select { width: 100%; max-width: 60rem; padding: .55rem; font: inherit
108
108
  .clarification-expected dt { font-weight: 600; margin-bottom: .3rem; }
109
109
  .clarification-expected dd { margin: 0; }
110
110
  .clarification-item label { font-weight: 600; margin-bottom: .3rem; }
111
+ .activity-list { display: grid; gap: .8rem; }
112
+ .activity-card { padding: 1rem; border: 1px solid color-mix(in srgb, CanvasText 14%, transparent); border-radius: 12px; }
113
+ .activity-card > :first-child, .approval-context > :first-child { margin-top: 0; }
114
+ .activity-card > :last-child, .approval-context > :last-child { margin-bottom: 0; }
115
+ .activity-card details { margin-top: .8rem; }
116
+ .approval-context { margin: .8rem 0; padding: .8rem 1rem; border-left: 4px solid color-mix(in srgb, Highlight 55%, GrayText); background: color-mix(in srgb, Highlight 7%, Canvas); }
117
+ .clarification-option-disposition { color: GrayText; font-size: .85rem; }
111
118
  .user-response-hint { flex-basis: 100%; margin: 0; color: GrayText; font-size: .9rem; }
112
119
  button { padding: .5rem 1rem; border-radius: 8px; border: 1px solid color-mix(in srgb, CanvasText 28%, transparent); background: color-mix(in srgb, CanvasText 5%, Canvas); color: CanvasText; font: inherit; cursor: pointer; }
113
120
  button:hover { background: color-mix(in srgb, CanvasText 12%, Canvas); }
@@ -150,6 +150,7 @@
150
150
  "questions-waiting-on-you": "Questions waiting on you",
151
151
  "count-questions-waiting-on-you": "{count} questions waiting on you",
152
152
  "your-answer-to-id": "Your answer to {id}",
153
+ "choose-one": "Choose one",
153
154
  "recommended": "Recommended",
154
155
  "scope-impact": "Scope",
155
156
  "added-work": "Added work",
@@ -322,7 +323,11 @@
322
323
  "open-decisions-before-approval": "Open decisions before approval",
323
324
  "no-open-decision-is-holding-approval": "No open decision is holding approval.",
324
325
  "uniform-verifier-label": "Uniform verifier",
325
- "uniform-verifier-legend": "This verifier answered the same verdict to every item it voted on, so this round's refutation signal came from its peers alone — the gate above reads as a wider cross-check than it was. A unanimous round is a legitimate outcome; confirm from the worker's audit sidecar that it actually opened the cited evidence."
326
+ "uniform-verifier-legend": "This verifier answered the same verdict to every item it voted on, so this round's refutation signal came from its peers alone — the gate above reads as a wider cross-check than it was. A unanimous round is a legitimate outcome; confirm from the worker's audit sidecar that it actually opened the cited evidence.",
327
+ "what-each-agent-did": "What each agent did",
328
+ "commands-and-evidence": "Commands and evidence",
329
+ "unblock-condition": "Unblock condition:",
330
+ "agent-evidence": "Agent evidence:"
326
331
  },
327
332
  "implementation": {
328
333
  "what-was-delivered": "What was delivered",
@@ -150,6 +150,7 @@
150
150
  "questions-waiting-on-you": "답변을 기다리는 질문",
151
151
  "count-questions-waiting-on-you": "답변을 기다리는 질문 {count}건",
152
152
  "your-answer-to-id": "{id}에 대한 답변",
153
+ "choose-one": "하나를 선택하세요",
153
154
  "recommended": "권장",
154
155
  "scope-impact": "범위",
155
156
  "added-work": "추가 작업",
@@ -322,7 +323,11 @@
322
323
  "open-decisions-before-approval": "승인 전에 남은 결정",
323
324
  "no-open-decision-is-holding-approval": "승인을 막는 미결 결정이 없습니다.",
324
325
  "uniform-verifier-label": "단일 판정 검증자",
325
- "uniform-verifier-legend": "이 검증자는 자기가 투표한 모든 항목에 같은 판정을 냈습니다. 그래서 이번 라운드의 반증 신호는 나머지 검증자에게서만 나왔고, 위의 게이트 값은 실제보다 넓은 교차 검증처럼 읽힙니다. 만장일치도 정당한 결과이므로, 그 워커의 audit 사이드카에서 인용한 근거를 실제로 열어봤는지 확인하십시오."
326
+ "uniform-verifier-legend": "이 검증자는 자기가 투표한 모든 항목에 같은 판정을 냈습니다. 그래서 이번 라운드의 반증 신호는 나머지 검증자에게서만 나왔고, 위의 게이트 값은 실제보다 넓은 교차 검증처럼 읽힙니다. 만장일치도 정당한 결과이므로, 그 워커의 audit 사이드카에서 인용한 근거를 실제로 열어봤는지 확인하십시오.",
327
+ "what-each-agent-did": "에이전트별 수행 내용",
328
+ "commands-and-evidence": "명령과 근거",
329
+ "unblock-condition": "승인 해제 조건:",
330
+ "agent-evidence": "에이전트 근거:"
326
331
  },
327
332
  "implementation": {
328
333
  "what-was-delivered": "무엇을 만들었나",
@@ -42,15 +42,27 @@
42
42
  <h2>{{ t('macros.forms.count-questions-waiting-on-you') | replace('{count}', items | length) }}</h2>
43
43
  <p class="clarification-lede">{{ t('macros.forms.the-code-alone-could-not-settle-these-fill-i') }} <strong>{{ t('macros.forms.export-my-answers') }}</strong> {{ t('macros.forms.at-the-foot-of-the-page') }}</p>
44
44
  {% for row in items %}
45
- <article class="clarification-item" id="id-{{ row.id }}" data-response-id="{{ row.id }}" data-kind="{{ row.kind }}">
45
+ {% set is_closed = row.status in ['resolved', 'obsolete'] %}
46
+ {% set approval_context = row.approvalContext | default(None) %}
47
+ <article class="clarification-item" id="id-{{ row.id }}" data-response-id="{{ row.id }}" data-kind="{{ row.kind }}" data-status="{{ row.status }}">
46
48
  <p class="eyebrow">{{ row.id }} · {{ row.kind }}</p>
47
49
  {{ row.statement | paragraphs }}
48
50
  <dl class="clarification-expected"><dt>{{ t('macros.forms.answer-as') }}</dt><dd>{{ row.expectedForm | inline_code }}</dd></dl>
51
+ {% if approval_context %}
52
+ <div class="approval-context" data-approval-classification="{{ approval_context.classification }}">
53
+ <p class="eyebrow">{{ approval_context.classification }}</p>
54
+ <p><strong>{{ t('tasks.implementation-planning.unblock-condition') }}</strong> {{ approval_context.unblockCondition | inline_code }}</p>
55
+ <p><strong>{{ t('tasks.implementation-planning.agent-evidence') }}</strong>
56
+ {% for activity_id in approval_context.activityIds %}<a href="#id-{{ activity_id }}">{{ activity_id }}</a>{% if not loop.last %}, {% endif %}{% endfor %}
57
+ </p>
58
+ </div>
59
+ {% endif %}
49
60
  {% if row.options %}
50
61
  <ol class="clarification-options">
51
62
  {% for option in row.options %}
52
63
  <li class="clarification-option{% if option.role == 'recommended' %} is-recommended{% endif %}">
53
64
  <p class="clarification-option-answer">{{ option.answer }}{% if option.role == 'recommended' %} <span class="badge">{{ t('macros.forms.recommended') }}</span>{% endif %}</p>
65
+ {% if option.disposition | default(None) %}<p class="clarification-option-disposition"><code>{{ option.disposition }}</code></p>{% endif %}
54
66
  <p class="clarification-option-rationale">{{ option.rationale }}</p>
55
67
  <dl class="clarification-option-impact">
56
68
  <dt>{{ t('macros.forms.scope-impact') }}</dt><dd>{{ option.scopeImpact | join(', ') }}</dd>
@@ -62,7 +74,14 @@
62
74
  </ol>
63
75
  {% endif %}
64
76
  <label for="response-{{ row.id }}">{{ t('macros.forms.your-answer-to-id') | replace('{id}', row.id) }}</label>
65
- <textarea id="response-{{ row.id }}" data-response-id="{{ row.id }}" rows="4">{{ row.userInput | default('') }}</textarea>
77
+ {% if row.kind == 'decision' and approval_context and row.options %}
78
+ <select id="response-{{ row.id }}" data-response-id="{{ row.id }}"{% if is_closed %} disabled{% endif %}>
79
+ <option value="">{{ t('macros.forms.choose-one') }}</option>
80
+ {% for option in row.options %}<option value="{{ option.answer }}" data-disposition="{{ option.disposition }}"{% if option.role == 'recommended' %} data-recommended="true"{% endif %}{% if row.userInput | default('') == option.answer %} selected{% endif %}>{{ option.answer }}</option>{% endfor %}
81
+ </select>
82
+ {% else %}
83
+ <textarea id="response-{{ row.id }}" data-response-id="{{ row.id }}" rows="4"{% if is_closed %} disabled{% endif %}>{{ row.userInput | default('') }}</textarea>
84
+ {% endif %}
66
85
  </article>
67
86
  {% endfor %}
68
87
  </section>
@@ -97,6 +97,31 @@
97
97
  <table data-report-field="implementationPlanning.skippedAdrCandidates"><thead><tr><th>{{ t('tasks.implementation-planning.topic') }}</th><th>{{ t('tasks.implementation-planning.why') }}</th></tr></thead><tbody>{% for row in planning.skippedAdrCandidates %}<tr><td>{{ row.topic | inline_code }}</td><td>{{ row.reason | inline_code }}</td></tr>{% endfor %}</tbody></table>{% endif %}
98
98
  </section>
99
99
 
100
+ {% if agentActivities %}
101
+ <section data-report-section="agent-activity">
102
+ <h2>{{ t('tasks.implementation-planning.what-each-agent-did') }}</h2>
103
+ <div class="activity-list">
104
+ {% for row in agentActivities %}
105
+ <article class="activity-card" id="id-{{ row.activityId }}">
106
+ <p class="eyebrow">{{ row.activityId }} · {{ row.agent }}</p>
107
+ <h3>{{ row.summary | inline_code }}</h3>
108
+ <p>{{ t('tasks.implementation-planning.result') }}: {{ row.outcome | inline_code }}</p>
109
+ <details>
110
+ <summary>{{ t('tasks.implementation-planning.commands-and-evidence') }}</summary>
111
+ <p><code>{{ row.kind }}</code></p>
112
+ {% if row.planItemIds %}<p>{{ row.planItemIds | join(', ') | inline_code }}</p>{% endif %}
113
+ {% for command in row.commands %}
114
+ <p><code>{{ command.command }}</code> · exit {{ command.exitCode }} · {{ command.outputSummary | inline_code }}</p>
115
+ {% endfor %}
116
+ {% if row.evidenceRefs %}<p>{{ row.evidenceRefs | join(', ') | inline_code }}</p>{% endif %}
117
+ {% if row.resultPath %}<p><code>{{ row.resultPath }}</code></p>{% endif %}
118
+ </details>
119
+ </article>
120
+ {% endfor %}
121
+ </div>
122
+ </section>
123
+ {% endif %}
124
+
100
125
  <section data-report-section="open-decisions">
101
126
  <h2>{{ t('tasks.implementation-planning.open-decisions-before-approval') }}</h2>
102
127
  <div class="summary-grid">{% for row in openDecisions %}{{ summary_card(row.id ~ " · " ~ row.kind, row.statement, "important", anchor=row.id) }}{% else %}<p>{{ t('tasks.implementation-planning.no-open-decision-is-holding-approval') }}</p>{% endfor %}</div>
@@ -60,6 +60,10 @@
60
60
  "readerSummary": "The shortest reader path: what was decided, what a human must do next, and which audit details can wait until a deeper review.",
61
61
  "endStateCoverage": "One row per end-state id the brief pinned, and how this phase accounted for it. A row that is not `addressed` must say why."
62
62
  },
63
+ "agentActivity": {
64
+ "heading": "Agent Activity Audit",
65
+ "intro": "Structured lead activity projected from this run's canonical event log."
66
+ },
63
67
  "analysis": {
64
68
  "basisIntro": "Immutable run snapshot, bounded scan scope, current-code facts, inferences, unknowns, and review disposition for this analysis.",
65
69
  "projectIntro": "Project components, boundaries, entry points, dependencies, stores, external systems, and the shallow feature index.",
@@ -44,33 +44,37 @@
44
44
  return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
45
45
  }
46
46
 
47
- // Read the user-supplied value out of one clarification row. Returns
48
- // the empty string when the row has no usable widget, the widget is
49
- // disabled, the select sits on its blank "(선택)" placeholder, or the
50
- // "기타" branch has an empty input. Caller decides whether to emit
51
- // an entry.
52
- function readRowValue(row) {
47
+ // Read the user-supplied value and decision effect from one clarification
48
+ // row. Returns null when the row has no usable input.
49
+ function readRowInput(row) {
53
50
  var sel = row.querySelector("select[data-response-id]");
54
51
  if (sel) {
55
- if (sel.disabled) return "";
52
+ if (sel.disabled) return null;
56
53
  var picked = sel.value;
57
- if (picked === "") return "";
54
+ if (picked === "") return null;
58
55
  if (picked === "__other__") {
59
56
  var rid = sel.getAttribute("data-response-id") || "";
60
57
  var other = row.querySelector('textarea[data-other-for="' + rid + '"]');
61
- return other ? trimMultiline(other.value) : "";
58
+ if (other) {
59
+ var otherValue = trimMultiline(other.value);
60
+ return otherValue ? { value: otherValue, disposition: "answer" } : null;
61
+ }
62
62
  }
63
63
  var opt = sel.options[sel.selectedIndex];
64
64
  // Use the visible option text ("(a) one-time backfill") so the
65
65
  // user-response sidecar stays human-readable, not just "a".
66
- return opt ? trimMultiline(opt.textContent) : picked;
66
+ return {
67
+ value: opt ? trimMultiline(opt.textContent) : picked,
68
+ disposition: opt ? (opt.getAttribute("data-disposition") || "answer") : "answer",
69
+ };
67
70
  }
68
71
  var ta = row.querySelector("textarea[data-response-id]");
69
72
  if (ta) {
70
- if (ta.disabled) return "";
71
- return trimMultiline(ta.value);
73
+ if (ta.disabled) return null;
74
+ var value = trimMultiline(ta.value);
75
+ return value ? { value: value, disposition: "answer" } : null;
72
76
  }
73
- return "";
77
+ return null;
74
78
  }
75
79
 
76
80
  function collectEntries() {
@@ -84,13 +88,14 @@
84
88
  );
85
89
  for (var i = 0; i < rows.length; i++) {
86
90
  var row = rows[i];
87
- var value = readRowValue(row);
88
- if (!value) continue;
91
+ var input = readRowInput(row);
92
+ if (!input) continue;
89
93
  entries.push({
90
94
  responseId: row.getAttribute("data-response-id") || "",
91
95
  kind: row.getAttribute("data-kind") || "",
92
- value: value,
96
+ value: input.value,
93
97
  rationale: null,
98
+ disposition: input.disposition,
94
99
  });
95
100
  }
96
101
  return entries;
@@ -460,11 +465,15 @@
460
465
  // globalThis (works under ESM where the parent uses `vm.runInThisContext`
461
466
  // — see tests/test_report_views.py for the byte-identity harness).
462
467
  if (typeof module !== "undefined" && module.exports) {
463
- module.exports = { buildUserResponseMarkdown: buildUserResponseMarkdown };
468
+ module.exports = {
469
+ buildUserResponseMarkdown: buildUserResponseMarkdown,
470
+ collectEntries: collectEntries,
471
+ };
464
472
  }
465
473
  if (typeof globalThis !== "undefined") {
466
474
  globalThis.__okstraReportViewExports__ = {
467
475
  buildUserResponseMarkdown: buildUserResponseMarkdown,
476
+ collectEntries: collectEntries,
468
477
  };
469
478
  }
470
479
  })();
@@ -25,12 +25,14 @@ The schema of each response block:
25
25
  ```markdown
26
26
  ## <Response ID>
27
27
  - Kind: <material | decision | data-point>
28
- - Disposition: <answer | reframe> # omit the line when answer (default). reframe = defer the answer + re-ask (row stays unresolved)
28
+ - Disposition: <answer | reframe | select | accept-risk | request-revision | reject> # omit the line when answer (default)
29
29
  - Value:
30
30
  > <multi-line value, trimmed>
31
31
  - Rationale: <optional one-line rationale>
32
32
  ```
33
33
 
34
+ `answer`, `select`, `accept-risk`, `request-revision`, and `reject` count as user answers when `Value` is non-empty. `reframe` alone does not count as an answer; it defers the answer and asks the next run to present the row again.
35
+
34
36
  An empty response set (when the user presses Export without filling in any row) outputs the following single line in the body:
35
37
 
36
38
  ```markdown
@@ -21,6 +21,14 @@ When `**Evidence ledger:** required-v1` is present, append one canonical row to
21
21
 
22
22
  Every file citation in the result MUST use backticks, a line suffix, and the same project-relative path its ledger row carries — for example `src/config/env.ts:1-22`. A bare filename (`env.ts:1-22`) does not match its row and fails exactly like a file you never opened, however many times you cited the full path earlier. A cited path without a matching ledger row fails Phase 7 in `validators/validate-run.py` `validate_worker_results_audit()`. Do not add a row for a file you did not open.
23
23
 
24
+ ## Evidence command ledger
25
+
26
+ Append one canonical JSON row to the audit sidecar for each command that produced or verified a conclusion:
27
+
28
+ - Evidence command: {"command":"npm run check","cwd":"<project-root>","exitCode":0,"outputSummary":"all checks passed"}
29
+
30
+ Do not record commands used only to explore, including `rg`, `ls`, and file-opening commands. Do not record environment variable values, tokens, credentials, or authorization headers. `okstra worker-audit-check` rejects malformed command rows and rows containing potential sensitive material through `okstra_ctl.worker_audit_ledger`.
31
+
24
32
  ## Anchor headers (lead-injected, BLOCKING)
25
33
 
26
34
  Every initial analysis prompt begins with these generated anchors in this exact order, before any other content: