forge-openclaw-plugin 0.2.86 → 0.2.89

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.
@@ -66,7 +66,7 @@ import { registerWebRoutes } from "./web.js";
66
66
  import { createManagerRuntime } from "./managers/runtime.js";
67
67
  import { isManagerError } from "./managers/type-guards.js";
68
68
  import { buildCompanionPairingTransport, getCompanionIrohStatus, stopCompanionIroh } from "./services/companion-iroh.js";
69
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getHealthZoneProfileForUser, getWorkoutSessionById, getWorkoutSessionDetailById, heartbeatCompanionPairing, heartbeatCompanionPairingSchema, healthZoneProfilePatchSchema, abortMobileHealthSyncSession, completeMobileHealthSyncSession, ingestMobileHealthSync, ingestMobileHealthSyncChunk, mobileHealthSyncChunkSchema, mobileHealthSyncSessionCompleteSchema, mobileHealthSyncSessionStartSchema, mobileHealthSyncSchema, patchHealthZoneProfileForUser, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, startMobileHealthSyncSession, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
69
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getHealthZoneProfileForUser, getMobileHealthSyncSessionStatus, getWorkoutSessionById, getWorkoutSessionDetailById, heartbeatCompanionPairing, heartbeatCompanionPairingSchema, healthZoneProfilePatchSchema, abortMobileHealthSyncSession, completeMobileHealthSyncSession, ingestMobileHealthSync, ingestMobileHealthSyncChunk, mobileHealthSyncChunkSchema, mobileHealthSyncSessionCompleteSchema, mobileHealthSyncSessionStartSchema, mobileHealthSyncSchema, patchHealthZoneProfileForUser, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, startMobileHealthSyncSession, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
70
70
  import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
71
71
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
72
72
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
@@ -279,6 +279,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
279
279
  relationshipRules: [
280
280
  "Every project belongs to a goal through goalId.",
281
281
  "Tasks can link to a project through projectId.",
282
+ "Projects can have one owner through userId plus one or more human or bot assignees through assigneeUserIds.",
283
+ "Projects are PRD-backed initiatives: productRequirementsDocument stores the project brief and workflowStatus stores the board lane separately from lifecycle status.",
282
284
  "Projects inherit strategic meaning from their parent goal."
283
285
  ],
284
286
  searchHints: [
@@ -315,6 +317,14 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
315
317
  enumValues: ["active", "paused", "completed"],
316
318
  defaultValue: "active"
317
319
  },
320
+ {
321
+ name: "workflowStatus",
322
+ type: "backlog|focus|in_progress|blocked|done",
323
+ required: false,
324
+ description: "Board workflow lane. Keep this separate from lifecycle status.",
325
+ enumValues: ["backlog", "focus", "in_progress", "blocked", "done"],
326
+ defaultValue: "backlog"
327
+ },
318
328
  {
319
329
  name: "userId",
320
330
  type: "string|null",
@@ -323,6 +333,26 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
323
333
  defaultValue: null,
324
334
  nullable: true
325
335
  },
336
+ {
337
+ name: "assigneeUserIds",
338
+ type: "string[]",
339
+ required: false,
340
+ description: "Human or bot user ids assigned to collaborate on the project.",
341
+ defaultValue: []
342
+ },
343
+ {
344
+ name: "productRequirementsDocument",
345
+ type: "string",
346
+ required: false,
347
+ description: "Project PRD or brief: outcome, scope, constraints, and acceptance direction.",
348
+ defaultValue: ""
349
+ },
350
+ {
351
+ name: "schedulingRules",
352
+ type: "CalendarSchedulingRules",
353
+ required: false,
354
+ description: "Optional calendar/work-block rules that govern when this project may be scheduled."
355
+ },
326
356
  {
327
357
  name: "targetPoints",
328
358
  type: "integer",
@@ -431,19 +461,24 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
431
461
  },
432
462
  {
433
463
  entityType: "task",
434
- purpose: "A concrete actionable work item. Tasks are what the user actually does.",
464
+ purpose: "A concrete actionable work item. The same stored family carries issue, task, and subtask levels; tasks are the one-session execution layer.",
435
465
  minimumCreateFields: ["title"],
436
466
  relationshipRules: [
437
- "A task can link to a goal, a project, both, or neither.",
467
+ "Use level=issue for vertical slices under a project, level=task for one focused AI or human work session, and level=subtask for lightweight child steps.",
468
+ "In hierarchy-aware Forge PM, issues live directly under projects, tasks live under issues, and subtasks live under tasks.",
469
+ "Legacy or inbox tasks may still link to a goal, project, both, or neither when the hierarchy is intentionally absent.",
470
+ "When placement matters, ask for the project, issue, or parent task that changes the hierarchy instead of asking a flat parent question.",
471
+ "Work items can have one owner through userId plus one or more human or bot assignees through assigneeUserIds.",
438
472
  "Live work is tracked by task runs, not by task status alone.",
439
473
  "A task status of in_progress does not guarantee a live active run."
440
474
  ],
441
475
  searchHints: [
442
476
  "Search by title before creating a duplicate task.",
443
- "Use linkedTo filters when you know the parent goal or project."
477
+ "Use linkedTo filters when you know the parent goal, project, issue, task, or subtask.",
478
+ "Use level and parentWorkItemId filters when the user is navigating the issue/task/subtask hierarchy."
444
479
  ],
445
480
  examples: [
446
- '{"title":"Write the plugin release notes","projectId":"project_forge_plugin_launch","status":"focus","priority":"high"}'
481
+ '{"title":"Add route-key example coverage","level":"task","projectId":"project_forge_plugin_launch","parentWorkItemId":"issue_question_flow_contract","aiInstructions":"Update the onboarding contract and tests so Workbench one-off execution has a route-key example.","status":"focus","priority":"high"}'
447
482
  ],
448
483
  fieldGuide: [
449
484
  {
@@ -459,6 +494,14 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
459
494
  description: "Markdown context, constraints, or acceptance notes.",
460
495
  defaultValue: ""
461
496
  },
497
+ {
498
+ name: "level",
499
+ type: "issue|task|subtask",
500
+ required: false,
501
+ description: "Work-item level. Use issue for vertical slices, task for one focused execution session, and subtask for lightweight child steps.",
502
+ enumValues: ["issue", "task", "subtask"],
503
+ defaultValue: "task"
504
+ },
462
505
  {
463
506
  name: "status",
464
507
  type: "backlog|focus|in_progress|blocked|done",
@@ -490,6 +533,13 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
490
533
  defaultValue: null,
491
534
  nullable: true
492
535
  },
536
+ {
537
+ name: "assigneeUserIds",
538
+ type: "string[]",
539
+ required: false,
540
+ description: "Human or bot user ids assigned to collaborate on this issue, task, or subtask.",
541
+ defaultValue: []
542
+ },
493
543
  {
494
544
  name: "goalId",
495
545
  type: "string|null",
@@ -506,6 +556,52 @@ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
506
556
  defaultValue: null,
507
557
  nullable: true
508
558
  },
559
+ {
560
+ name: "parentWorkItemId",
561
+ type: "string|null",
562
+ required: false,
563
+ description: "Parent issue id for level=task or parent task id for level=subtask. Issues should sit directly under projectId.",
564
+ defaultValue: null,
565
+ nullable: true
566
+ },
567
+ {
568
+ name: "aiInstructions",
569
+ type: "string",
570
+ required: false,
571
+ description: "Execution instructions for the agent or human doing the one-session task.",
572
+ defaultValue: ""
573
+ },
574
+ {
575
+ name: "executionMode",
576
+ type: "afk|hitl|null",
577
+ required: false,
578
+ description: "Whether the issue or task is intended for autonomous work or human-in-the-loop work.",
579
+ enumValues: ["afk", "hitl"],
580
+ defaultValue: null,
581
+ nullable: true
582
+ },
583
+ {
584
+ name: "acceptanceCriteria",
585
+ type: "string[]",
586
+ required: false,
587
+ description: "Structured success criteria, usually Given/When/Then for issues or concrete done checks for tasks.",
588
+ defaultValue: []
589
+ },
590
+ {
591
+ name: "blockerLinks",
592
+ type: "Array<{ entityType, entityId, label? }>",
593
+ required: false,
594
+ description: "Optional blockers linked to Forge entities that make the work unable to proceed.",
595
+ defaultValue: []
596
+ },
597
+ {
598
+ name: "completionReport",
599
+ type: "{ modifiedFiles: string[], workSummary: string, linkedGitRefIds: string[] }|null",
600
+ required: false,
601
+ description: "Closeout report for completed work items, especially tasks finished by agents.",
602
+ defaultValue: null,
603
+ nullable: true
604
+ },
509
605
  {
510
606
  name: "dueDate",
511
607
  type: "YYYY-MM-DD|null",
@@ -3007,10 +3103,11 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3007
3103
  "Ask what this piece of work is trying to make true.",
3008
3104
  "Reflect the emerging boundary so the user can hear what is in scope.",
3009
3105
  "Ask what outcome would make the project feel real or complete for now.",
3106
+ "Ask what belongs in the project PRD or brief when the user is shaping delivery rather than only naming a project.",
3010
3107
  "Ask what belongs inside the boundary and what can stay out if the scope still feels muddy.",
3011
3108
  "Ask which goal it belongs under.",
3012
3109
  "Land on a working name once the scope is clear.",
3013
- "Clarify status, owner, and notes only after the scope is clear."
3110
+ "Clarify lifecycle status, workflow lane, owner, human/bot assignees, scheduling rules, and notes only after the scope is clear."
3014
3111
  ]
3015
3112
  },
3016
3113
  {
@@ -3028,11 +3125,13 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3028
3125
  {
3029
3126
  focus: "task",
3030
3127
  openingQuestion: "What is the next concrete move here?",
3031
- coachingGoal: "Identify the next concrete move, not just capture a vague obligation.",
3128
+ coachingGoal: "Identify the next concrete one-session work item and place it in the issue/task/subtask hierarchy when that hierarchy matters.",
3032
3129
  askSequence: [
3033
3130
  "Ask what the next concrete action is.",
3034
- "Ask where it belongs: project, goal, both, or standalone.",
3035
- "Ask what would make it easier to do: due date, priority, owner, or brief context."
3131
+ "Ask whether this is an issue, one-session task, or subtask only when the level is not already obvious.",
3132
+ "Ask where it belongs in the hierarchy: project for an issue, issue for a task, or parent task for a subtask. Use goal or standalone only when the user is intentionally outside the PM hierarchy.",
3133
+ "Capture the execution contract in aiInstructions when the task is for an agent session.",
3134
+ "Ask what would make it easier to do: due date, priority, owner, human/bot assignees, acceptance criteria, or brief context."
3036
3135
  ]
3037
3136
  },
3038
3137
  {
@@ -3499,6 +3598,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3499
3598
  "Use an ACT-style values clarification stance: values are directions to live toward, not boxes to complete.",
3500
3599
  "Ask one or two questions at a time, reflect back the user's language, and only then move toward naming committed actions or linked work items.",
3501
3600
  "Reflect the pain, longing, or importance that makes the value alive before narrowing to action.",
3601
+ "Once one ordinary moment is clear, offer one tentative hypothesis about the pain, longing, or value conflict that makes this value alive now, then ask whether it lands before turning it into action wording.",
3502
3602
  "If the user says they want to understand it first, start with one orienting question before offering a formulation or save suggestion."
3503
3603
  ]
3504
3604
  },
@@ -3585,7 +3685,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3585
3685
  "When the behavior clearly belongs inside a larger loop, suggest linking or also mapping the related behavior_pattern.",
3586
3686
  "If the user asks for understanding before storage, ask about the recent example and function of the move before classifying it.",
3587
3687
  "Ask what the move is trying to do for the user before moving into replacement planning.",
3588
- "Name the immediate protective job before discussing costs or alternatives."
3688
+ "Name the immediate protective job before discussing costs or alternatives.",
3689
+ "Once one example is clear, offer one tentative hypothesis about the immediate problem the behavior solves, the cue or urge that pulls it online, and the cost it creates later."
3589
3690
  ]
3590
3691
  },
3591
3692
  {
@@ -3703,7 +3804,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3703
3804
  notes: [
3704
3805
  "A mode_guide_session is the exploration worksheet, not the final identity claim.",
3705
3806
  "Store the user's answers faithfully and keep interpretations tentative unless the user wants a durable mode_profile.",
3706
- "Use candidate mode interpretations as testable hypotheses tied to the user's answers, not as certain labels."
3807
+ "Use candidate mode interpretations as testable hypotheses tied to the user's answers, not as certain labels.",
3808
+ "After enough answers are visible, offer one hypothesis about what the active part is trying to stop, force, prevent, or secure before proposing any durable mode label."
3707
3809
  ]
3708
3810
  },
3709
3811
  {
@@ -3750,6 +3852,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3750
3852
  "Use shared batch entity routes for flashcard create, update, delete, restore, and search.",
3751
3853
  "When the user says they feel an urge or trigger, search flashcards by triggerSentence, triggerSituation, tags, message, and linked Psyche records before creating a new card.",
3752
3854
  "When showing a card, display or quote the message first, then add brief grounding, urge-surfing, cognitive defusion, schema-mode, or values-based coaching around it.",
3855
+ "If the cue, urge sentence, mode, belief, or value pivot is clear, offer one hypothesis about what the card needs to meet in the hard moment before polishing its final wording.",
3753
3856
  "Do not make the user fill styling fields before the therapeutic sentence is clear."
3754
3857
  ]
3755
3858
  },
@@ -3830,7 +3933,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3830
3933
  notes: [
3831
3934
  "event_type is a Psyche taxonomy record, but it still needs active listening because it names emotionally meaningful episodes.",
3832
3935
  "Do not open with pure label wording unless the lived category and boundary are already clear.",
3833
- "Offer a candidate label after the repeated moment is understood, and keep the user's wording when it already fits."
3936
+ "Offer a candidate label after the repeated moment is understood, and keep the user's wording when it already fits.",
3937
+ "Once one recurring example is clear, offer one hypothesis about the repeated emotional or relational stake that future reports need this event type to preserve."
3834
3938
  ]
3835
3939
  },
3836
3940
  {
@@ -3864,6 +3968,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3864
3968
  notes: [
3865
3969
  "emotion_definition is a Psyche taxonomy record, not a generic dictionary entry.",
3866
3970
  "Start from the lived feeling before asking for category or browsing fields.",
3971
+ "Once the lived signature is visible, offer one hypothesis about what the emotion warns about, protects, demands, or longs for before saving the reusable definition.",
3867
3972
  "If the user already gives a good label, reflect the felt signature and ask only for the one boundary or definition detail that still matters."
3868
3973
  ]
3869
3974
  }
@@ -4501,11 +4606,13 @@ function buildAgentOnboardingPayload(request) {
4501
4606
  entityConversationPlaybooks: AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS.map(enrichConversationPlaybookWithRouteInfo),
4502
4607
  relationshipModel: [
4503
4608
  "Every Forge record belongs to one typed user owner: either human or bot.",
4609
+ "Projects and work items can also carry assigneeUserIds for one or more human or bot collaborators; ownership and assignment are separate.",
4504
4610
  "Read routes may scope to one user with userId or to several users with repeated userIds.",
4505
4611
  "Ownership and linkage are separate: a human-owned project can link to bot-owned tasks, strategies, notes, or insights.",
4506
4612
  "Goals are the top-level strategic layer.",
4507
4613
  "Projects belong to one goal through goalId.",
4508
- "Tasks can belong to a goal, a project, both, or neither.",
4614
+ "The task entity is the stored work-item family for issue, task, and subtask levels.",
4615
+ "Hierarchy-aware PM should place issues under projects, tasks under issues, and subtasks under tasks; standalone or goal-linked tasks are legacy/inbox escape hatches rather than the default planning path.",
4509
4616
  "Strategies can target one or many goals or projects while sequencing project and task nodes through a directed acyclic graph.",
4510
4617
  "A strategy remains editable until it is locked. Once locked, the plan becomes a contract and graph-shape edits should stop until the strategy is explicitly unlocked.",
4511
4618
  "Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
@@ -5114,6 +5221,8 @@ function buildAgentOnboardingPayload(request) {
5114
5221
  reviewShortcutRule: "When the user is reviewing or correcting an existing record, ask what practical question they want the read or correction to answer, then narrow the saved object, timeframe, or route family first. Do not reopen the whole intake unless the user is actually redefining the record.",
5115
5222
  readModelWriteRule: "Self-observation is note-backed and should be written through observed notes with frontmatter.observedAt only when a lightweight episode observation is the right container. Do not use it as the default bucket for Psyche material: prefer trigger_report for one emotionally meaningful episode, behavior_pattern for functional analysis of a recurring loop, behavior for one repeated move, belief_entry for a core sentence, mode_guide_session or mode_profile for a central part-state, and wiki_page for durable memory such as books, articles, concepts, sources, or personal manuals. Sleep and workout sessions stay on batch CRUD by default; use the reflective review helpers only when enriching one already-known record after review.",
5116
5223
  psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
5224
+ followUpQuestionRule: "After a substantive answer, do not restart the opener or jump to the next schema field. First say what became clearer in concrete language, then choose exactly one next lane: wording, boundary, placement, timing, route scope, link, hypothesis, or write confirmation. Ask the smallest question that would change the record shape, route choice, useful wording, timing, or links. If nothing decision-relevant would change, stop asking, summarize the working record, and act with consent.",
5225
+ antiDriftRule: "Avoid vague reflective filler and internal route language. Replace phrases like 'that sounds important' with the specific stake you heard, and replace API nouns like surface, CRUD, payload, mutation path, or endpoint with user-facing product nouns such as belief, pattern, note, wiki page, timeline, overlay, weekday template, flow, run, node result, or published output. If a question would only decorate the intake, skip it.",
5117
5226
  duplicateCheckRoute: "/api/v1/entities/search",
5118
5227
  uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
5119
5228
  browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job. Batch CRUD is the default for simple entities; avoid spamming the agent with a large one-route-per-entity mental model.",
@@ -5161,6 +5270,7 @@ function buildAgentOnboardingPayload(request) {
5161
5270
  workbenchPublishedOutput: '{"routeKey":"publishedOutput","pathParams":{"id":"flow_research_digest"}}',
5162
5271
  workbenchLatestNodeOutput: '{"routeKey":"latestNodeOutput","pathParams":{"id":"flow_research_digest","nodeId":"node_summary"}}',
5163
5272
  workbenchRunFlow: '{"routeKey":"runFlow","pathParams":{"id":"flow_research_digest"},"body":{"input":{"topic":"question flow quality"}}}',
5273
+ workbenchRunByPayload: '{"routeKey":"runByPayload","body":{"flow":{"title":"One-off digest","nodes":[]},"input":{"topic":"question flow quality"}}}',
5164
5274
  workbenchChatFlow: '{"routeKey":"chatFlow","pathParams":{"id":"flow_research_digest"},"body":{"message":"Refine the summary around API route risks and keep the published output stable."}}'
5165
5275
  }
5166
5276
  }
@@ -7670,6 +7780,18 @@ export async function buildServer(options = {}) {
7670
7780
  app.post("/api/v1/mobile/healthkit/sync-sessions", async (request) => ({
7671
7781
  upload: startMobileHealthSyncSession(mobileHealthSyncSessionStartSchema.parse(request.body ?? {}))
7672
7782
  }));
7783
+ app.get("/api/v1/mobile/healthkit/sync-sessions/:id", async (request) => {
7784
+ const { id } = request.params;
7785
+ const query = z
7786
+ .object({
7787
+ sessionId: z.string().trim().min(1),
7788
+ pairingToken: z.string().trim().min(1)
7789
+ })
7790
+ .parse(request.query ?? {});
7791
+ return {
7792
+ upload: getMobileHealthSyncSessionStatus(id, query)
7793
+ };
7794
+ });
7673
7795
  app.post("/api/v1/mobile/healthkit/sync-sessions/:id/chunks", { bodyLimit: 40_000_000 }, async (request) => {
7674
7796
  const { id } = request.params;
7675
7797
  const rawPayloadJson = JSON.stringify((request.body ?? {}).payload ?? {});
@@ -2714,6 +2714,57 @@ function readMobileSyncSession(syncSessionId) {
2714
2714
  .prepare(`SELECT * FROM health_mobile_sync_sessions WHERE id = ?`)
2715
2715
  .get(syncSessionId);
2716
2716
  }
2717
+ function mobileSyncSessionProgress(syncSessionId) {
2718
+ const chunks = getDatabase()
2719
+ .prepare(`SELECT family, record_count, byte_count
2720
+ FROM health_mobile_sync_chunks
2721
+ WHERE sync_session_id = ?`)
2722
+ .all(syncSessionId);
2723
+ const receivedCounts = {};
2724
+ const byteTotals = {};
2725
+ for (const chunk of chunks) {
2726
+ receivedCounts[chunk.family] =
2727
+ (receivedCounts[chunk.family] ?? 0) + chunk.record_count;
2728
+ byteTotals[chunk.family] =
2729
+ (byteTotals[chunk.family] ?? 0) + chunk.byte_count;
2730
+ }
2731
+ return {
2732
+ receivedCounts,
2733
+ byteTotals,
2734
+ chunkCount: chunks.length,
2735
+ receivedBytes: chunks.reduce((sum, chunk) => sum + chunk.byte_count, 0)
2736
+ };
2737
+ }
2738
+ function mobileSyncSessionUploadPayload(session, receivedChunkIds) {
2739
+ return {
2740
+ syncSessionId: session.id,
2741
+ schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
2742
+ status: session.status,
2743
+ chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
2744
+ chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
2745
+ chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
2746
+ acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
2747
+ supportsCompression: true,
2748
+ acceptedFamilies: safeJsonParse(session.requested_families_json, []),
2749
+ receivedChunkIds,
2750
+ progress: mobileSyncSessionProgress(session.id)
2751
+ };
2752
+ }
2753
+ export function getMobileHealthSyncSessionStatus(syncSessionId, payload) {
2754
+ const pairing = requireValidPairing(payload.sessionId, payload.pairingToken);
2755
+ const session = readMobileSyncSession(syncSessionId);
2756
+ if (!session || session.pairing_session_id !== pairing.id) {
2757
+ throw new HttpError(404, "sync_session_not_found", "The HealthKit sync session does not exist.");
2758
+ }
2759
+ const receivedChunkIds = getDatabase()
2760
+ .prepare(`SELECT chunk_id
2761
+ FROM health_mobile_sync_chunks
2762
+ WHERE sync_session_id = ?
2763
+ ORDER BY sequence ASC`)
2764
+ .all(syncSessionId)
2765
+ .map((row) => row.chunk_id);
2766
+ return mobileSyncSessionUploadPayload(session, receivedChunkIds);
2767
+ }
2717
2768
  function ensureRunningMobileSyncSession(syncSessionId) {
2718
2769
  expireStaleMobileSyncSessions();
2719
2770
  const session = readMobileSyncSession(syncSessionId);
@@ -3038,30 +3089,13 @@ function applyWorkoutEvidenceChunkImmediately(session, payload) {
3038
3089
  });
3039
3090
  }
3040
3091
  function updateMobileSyncSessionProgress(syncSessionId) {
3041
- const chunks = getDatabase()
3042
- .prepare(`SELECT family, record_count, byte_count
3043
- FROM health_mobile_sync_chunks
3044
- WHERE sync_session_id = ?`)
3045
- .all(syncSessionId);
3046
- const receivedCounts = {};
3047
- const byteTotals = {};
3048
- for (const chunk of chunks) {
3049
- receivedCounts[chunk.family] =
3050
- (receivedCounts[chunk.family] ?? 0) + chunk.record_count;
3051
- byteTotals[chunk.family] =
3052
- (byteTotals[chunk.family] ?? 0) + chunk.byte_count;
3053
- }
3092
+ const progress = mobileSyncSessionProgress(syncSessionId);
3054
3093
  getDatabase()
3055
3094
  .prepare(`UPDATE health_mobile_sync_sessions
3056
3095
  SET received_counts_json = ?, byte_totals_json = ?, updated_at = ?
3057
3096
  WHERE id = ?`)
3058
- .run(JSON.stringify(receivedCounts), JSON.stringify(byteTotals), nowIso(), syncSessionId);
3059
- return {
3060
- receivedCounts,
3061
- byteTotals,
3062
- chunkCount: chunks.length,
3063
- receivedBytes: chunks.reduce((sum, chunk) => sum + chunk.byte_count, 0)
3064
- };
3097
+ .run(JSON.stringify(progress.receivedCounts), JSON.stringify(progress.byteTotals), nowIso(), syncSessionId);
3098
+ return progress;
3065
3099
  }
3066
3100
  export function startMobileHealthSyncSession(payload) {
3067
3101
  const parsed = mobileHealthSyncSessionStartSchema.parse(payload);
@@ -3092,17 +3126,7 @@ export function startMobileHealthSyncSession(payload) {
3092
3126
  ORDER BY sequence ASC`)
3093
3127
  .all(resumeSyncSessionId)
3094
3128
  .map((row) => row.chunk_id);
3095
- return {
3096
- syncSessionId: resumeSyncSessionId,
3097
- schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
3098
- chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
3099
- chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
3100
- chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
3101
- acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
3102
- supportsCompression: true,
3103
- acceptedFamilies: existingFamilies,
3104
- receivedChunkIds
3105
- };
3129
+ return mobileSyncSessionUploadPayload(existing, receivedChunkIds);
3106
3130
  }
3107
3131
  getDatabase()
3108
3132
  .prepare(`UPDATE health_mobile_sync_sessions
@@ -3127,17 +3151,11 @@ export function startMobileHealthSyncSession(payload) {
3127
3151
  sourceStates: parsed.sourceStates,
3128
3152
  metadata: parsed.metadata
3129
3153
  }), JSON.stringify(parsed.expectedCounts), now, now, now);
3130
- return {
3131
- syncSessionId,
3132
- schemaVersion: HEALTH_MOBILE_SYNC_SCHEMA_VERSION,
3133
- chunkTargetBytes: HEALTH_MOBILE_SYNC_CHUNK_TARGET_BYTES,
3134
- chunkMaxBytes: HEALTH_MOBILE_SYNC_CHUNK_MAX_BYTES,
3135
- chunkPayloadEncoding: HEALTH_MOBILE_SYNC_CHUNK_PAYLOAD_ENCODING,
3136
- acceptedPayloadEncodings: HEALTH_MOBILE_SYNC_ACCEPTED_CHUNK_PAYLOAD_ENCODINGS,
3137
- supportsCompression: true,
3138
- acceptedFamilies: parsed.requestedFamilies,
3139
- receivedChunkIds: []
3140
- };
3154
+ const session = readMobileSyncSession(syncSessionId);
3155
+ if (!session) {
3156
+ throw new HttpError(500, "sync_session_not_found", "The HealthKit sync session could not be created.");
3157
+ }
3158
+ return mobileSyncSessionUploadPayload(session, []);
3141
3159
  }
3142
3160
  export function ingestMobileHealthSyncChunk(syncSessionId, payload, rawPayloadJson) {
3143
3161
  const parsed = mobileHealthSyncChunkSchema.parse(payload);
@@ -28,6 +28,49 @@ const HTTP_METHODS = new Set([
28
28
  "options",
29
29
  "head"
30
30
  ]);
31
+ const mobileHealthSyncProgressSchema = {
32
+ type: "object",
33
+ required: [
34
+ "chunkCount",
35
+ "receivedBytes",
36
+ "receivedCounts",
37
+ "byteTotals"
38
+ ],
39
+ properties: {
40
+ chunkCount: { type: "number" },
41
+ receivedBytes: { type: "number" },
42
+ receivedCounts: { type: "object", additionalProperties: { type: "number" } },
43
+ byteTotals: { type: "object", additionalProperties: { type: "number" } }
44
+ }
45
+ };
46
+ const mobileHealthSyncUploadSchema = {
47
+ type: "object",
48
+ required: [
49
+ "syncSessionId",
50
+ "schemaVersion",
51
+ "status",
52
+ "targetChunkBytes",
53
+ "maxChunkBytes",
54
+ "payloadEncodings",
55
+ "acceptedFamilies",
56
+ "receivedChunkIds",
57
+ "progress"
58
+ ],
59
+ properties: {
60
+ syncSessionId: { type: "string" },
61
+ schemaVersion: { type: "string" },
62
+ status: {
63
+ type: "string",
64
+ enum: ["running", "completed", "failed", "aborted"]
65
+ },
66
+ targetChunkBytes: { type: "number" },
67
+ maxChunkBytes: { type: "number" },
68
+ payloadEncodings: arrayOf({ type: "string" }),
69
+ acceptedFamilies: arrayOf({ type: "string" }),
70
+ receivedChunkIds: arrayOf({ type: "string" }),
71
+ progress: mobileHealthSyncProgressSchema
72
+ }
73
+ };
31
74
  const CALENDAR_PROVIDER_VALUES = [
32
75
  "google",
33
76
  "apple",
@@ -4052,6 +4095,8 @@ export function buildOpenApiDocument() {
4052
4095
  "reviewShortcutRule",
4053
4096
  "readModelWriteRule",
4054
4097
  "psycheOpeningQuestionRule",
4098
+ "followUpQuestionRule",
4099
+ "antiDriftRule",
4055
4100
  "duplicateCheckRoute",
4056
4101
  "uiSuggestionRule",
4057
4102
  "browserFallbackRule",
@@ -4067,6 +4112,8 @@ export function buildOpenApiDocument() {
4067
4112
  reviewShortcutRule: { type: "string" },
4068
4113
  readModelWriteRule: { type: "string" },
4069
4114
  psycheOpeningQuestionRule: { type: "string" },
4115
+ followUpQuestionRule: { type: "string" },
4116
+ antiDriftRule: { type: "string" },
4070
4117
  duplicateCheckRoute: { type: "string" },
4071
4118
  uiSuggestionRule: { type: "string" },
4072
4119
  browserFallbackRule: { type: "string" },
@@ -5291,6 +5338,144 @@ export function buildOpenApiDocument() {
5291
5338
  }
5292
5339
  }
5293
5340
  },
5341
+ "/api/v1/mobile/healthkit/sync-sessions": {
5342
+ post: {
5343
+ summary: "Start or resume a resumable mobile HealthKit upload session",
5344
+ requestBody: {
5345
+ required: true,
5346
+ content: {
5347
+ "application/json": {
5348
+ schema: {
5349
+ type: "object",
5350
+ required: ["sessionId", "pairingToken", "schemaVersion"],
5351
+ properties: {
5352
+ sessionId: { type: "string" },
5353
+ pairingToken: { type: "string" },
5354
+ schemaVersion: { type: "string" },
5355
+ resumeSyncSessionId: nullable({ type: "string" }),
5356
+ requestedFamilies: arrayOf({ type: "string" }),
5357
+ sourceStates: arrayOf({
5358
+ type: "object",
5359
+ additionalProperties: true
5360
+ }),
5361
+ device: {
5362
+ type: "object",
5363
+ additionalProperties: true
5364
+ }
5365
+ }
5366
+ }
5367
+ }
5368
+ }
5369
+ },
5370
+ responses: {
5371
+ "200": jsonResponse({
5372
+ type: "object",
5373
+ required: ["upload"],
5374
+ properties: {
5375
+ upload: mobileHealthSyncUploadSchema
5376
+ }
5377
+ }, "Accepted upload session plus received chunk progress")
5378
+ }
5379
+ }
5380
+ },
5381
+ "/api/v1/mobile/healthkit/sync-sessions/{id}": {
5382
+ get: {
5383
+ summary: "Inspect a mobile HealthKit upload session and accepted chunk progress",
5384
+ parameters: [
5385
+ {
5386
+ name: "id",
5387
+ in: "path",
5388
+ required: true,
5389
+ schema: { type: "string" }
5390
+ },
5391
+ {
5392
+ name: "sessionId",
5393
+ in: "query",
5394
+ required: true,
5395
+ schema: { type: "string" }
5396
+ },
5397
+ {
5398
+ name: "pairingToken",
5399
+ in: "query",
5400
+ required: true,
5401
+ schema: { type: "string" }
5402
+ }
5403
+ ],
5404
+ responses: {
5405
+ "200": jsonResponse({
5406
+ type: "object",
5407
+ required: ["upload"],
5408
+ properties: {
5409
+ upload: mobileHealthSyncUploadSchema
5410
+ }
5411
+ }, "Upload session status and accepted chunk progress")
5412
+ }
5413
+ }
5414
+ },
5415
+ "/api/v1/mobile/healthkit/sync-sessions/{id}/chunks": {
5416
+ post: {
5417
+ summary: "Upload one idempotent HealthKit chunk into a resumable session",
5418
+ parameters: [
5419
+ {
5420
+ name: "id",
5421
+ in: "path",
5422
+ required: true,
5423
+ schema: { type: "string" }
5424
+ }
5425
+ ],
5426
+ requestBody: {
5427
+ required: true,
5428
+ content: {
5429
+ "application/json": {
5430
+ schema: {
5431
+ type: "object",
5432
+ required: [
5433
+ "sessionId",
5434
+ "pairingToken",
5435
+ "chunkId",
5436
+ "family",
5437
+ "sequence",
5438
+ "records",
5439
+ "byteCount",
5440
+ "checksum"
5441
+ ],
5442
+ properties: {
5443
+ sessionId: { type: "string" },
5444
+ pairingToken: { type: "string" },
5445
+ chunkId: { type: "string" },
5446
+ family: { type: "string" },
5447
+ sequence: { type: "number" },
5448
+ records: { type: "number" },
5449
+ byteCount: { type: "number" },
5450
+ checksum: { type: "string" },
5451
+ compression: nullable({ type: "string" }),
5452
+ payloadEncoding: nullable({ type: "string" }),
5453
+ payload: {}
5454
+ }
5455
+ }
5456
+ }
5457
+ }
5458
+ },
5459
+ responses: {
5460
+ "200": jsonResponse({
5461
+ type: "object",
5462
+ required: ["chunk"],
5463
+ properties: {
5464
+ chunk: {
5465
+ type: "object",
5466
+ additionalProperties: true,
5467
+ required: ["accepted", "duplicate", "progress"],
5468
+ properties: {
5469
+ accepted: { type: "boolean" },
5470
+ duplicate: { type: "boolean" },
5471
+ progress: mobileHealthSyncProgressSchema
5472
+ }
5473
+ }
5474
+ }
5475
+ }, "Chunk receipt with duplicate detection and aggregate progress")
5476
+ }
5477
+ }
5478
+ },
5294
5479
  "/api/v1/doctor": {
5295
5480
  get: {
5296
5481
  summary: "Run Forge Doctor diagnostics for runtime, settings, storage, entities, hierarchy, rewards, and fix proposals",
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.86",
5
+ "version": "0.2.89",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.86",
3
+ "version": "0.2.89",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -145,6 +145,8 @@ Concrete route-key examples for internal use:
145
145
  `{"routeKey":"latestNodeOutput","pathParams":{"id":"flow_research_digest","nodeId":"node_summary"}}`
146
146
  - Workbench run execution:
147
147
  `{"routeKey":"runFlow","pathParams":{"id":"flow_research_digest"},"body":{"input":{"topic":"question flow quality"}}}`
148
+ - Workbench one-off input execution:
149
+ `{"routeKey":"runByPayload","body":{"flow":{"title":"One-off digest","nodes":[]},"input":{"topic":"question flow quality"}}}`
148
150
  - Workbench flow chat follow-up:
149
151
  `{"routeKey":"chatFlow","pathParams":{"id":"flow_research_digest"},"body":{"message":"Refine the summary around API route risks and keep the published output stable."}}`
150
152
 
@@ -349,7 +351,8 @@ Only ask if missing or unclear:
349
351
  `project`
350
352
  Use for a bounded workstream under a goal.
351
353
  Minimum field: `title`
352
- Usually useful: `goalId`, `description`, `status`
354
+ Usually useful: `goalId`, `description`, `productRequirementsDocument`,
355
+ `status`, `workflowStatus`, `userId`, `assigneeUserIds`, `schedulingRules`
353
356
  Only ask if missing or unclear:
354
357
 
355
358
  1. What should this project be called?
@@ -357,14 +360,18 @@ Only ask if missing or unclear:
357
360
  3. What outcome should it produce?
358
361
 
359
362
  `task`
360
- Use for one concrete action or deliverable.
363
+ Use for one concrete work item; the same stored family carries `issue`, `task`, and
364
+ `subtask` levels.
361
365
  Minimum field: `title`
362
- Usually useful: `projectId`, `goalId`, `priority`, `dueDate`, `status`, `owner`
366
+ Usually useful: `level`, `projectId`, `parentWorkItemId`, `aiInstructions`,
367
+ `executionMode`, `acceptanceCriteria`, `priority`, `dueDate`, `status`, `userId`,
368
+ `assigneeUserIds`
363
369
  Only ask if missing or unclear:
364
370
 
365
371
  1. What is the task in one concrete sentence?
366
- 2. Should it live under an existing goal or project?
367
- 3. Does it need a due date, priority, or owner?
372
+ 2. Is this an issue, one-session task, or subtask?
373
+ 3. Should it live under a project, issue, parent task, or intentional inbox?
374
+ 4. Does it need agent instructions, acceptance criteria, a due date, priority, owner, or assignees?
368
375
 
369
376
  `habit`
370
377
  Use for a recurring commitment or recurring slip with explicit cadence and XP consequences.
@@ -370,6 +370,23 @@ Closing turn:
370
370
  - ask whether it feels true enough to save or needs one correction
371
371
  - if the user says yes, move to the write instead of reopening the intake
372
372
 
373
+ ## Second-turn discipline
374
+
375
+ After the user answers the opening question, do not restart the opener and do not
376
+ jump to the next schema field. First say what became clearer in concrete language,
377
+ then choose exactly one next lane: wording, boundary, placement, timing, route scope,
378
+ link, hypothesis, or write confirmation.
379
+
380
+ The second question should be the smallest question that would change the record
381
+ shape, route choice, useful wording, timing, or links. If no answer would change one
382
+ of those things, stop asking, summarize the working record, and act with consent.
383
+
384
+ Do not drift into vague reflection or internal route language. Replace "that sounds
385
+ important" with the specific stake you heard, and replace API words such as surface,
386
+ CRUD, payload, mutation path, or endpoint with product nouns the user recognizes:
387
+ belief, pattern, note, wiki page, timeline, overlay, weekday template, flow, run,
388
+ node result, or published output.
389
+
373
390
  ## Steering moves
374
391
 
375
392
  Use these small moves to keep the intake natural and intentional.
@@ -690,17 +707,23 @@ Arc:
690
707
  1. Ask what this piece of work is trying to make true.
691
708
  2. Reflect the emerging boundary so the user can hear what is in scope.
692
709
  3. Ask what outcome would make it feel real or complete for now.
693
- 4. Ask what belongs inside the boundary and what can stay out if the scope still
710
+ 4. Ask what belongs in the project PRD or brief when the user is shaping delivery
711
+ rather than only naming a project.
712
+ 5. Ask what belongs inside the boundary and what can stay out if the scope still
694
713
  feels muddy.
695
- 5. Ask which goal it belongs under.
696
- 6. Land on a working name once the scope is clear.
697
- 7. Clarify status, owner, and notes only after the scope is clear.
714
+ 6. Ask which goal it belongs under.
715
+ 7. Land on a working name once the scope is clear.
716
+ 8. Clarify lifecycle status, workflow lane, owner, human/bot assignees, scheduling
717
+ rules, and notes only after the scope is clear.
698
718
 
699
719
  Helpful follow-up lanes:
700
720
 
701
721
  - what concrete outcome would make this project complete enough
722
+ - what should go into the PRD or brief
702
723
  - what belongs inside the boundary and what does not
703
724
  - which goal gives the project meaning
725
+ - whether one owner or several human/bot assignees need to be explicit
726
+ - whether scheduling rules or a board workflow lane matter now
704
727
 
705
728
  Ready to save when:
706
729
 
@@ -744,26 +767,37 @@ Preferred opening question:
744
767
 
745
768
  ## Task
746
769
 
747
- Aim: identify the next concrete move, not just capture a vague obligation.
770
+ Aim: identify the next concrete one-session work item and place it correctly in the
771
+ issue/task/subtask hierarchy when that hierarchy matters.
748
772
 
749
773
  Arc:
750
774
 
751
775
  1. Ask what the next concrete action is.
752
- 2. Ask where it belongs: project, goal, both, or standalone.
753
- 3. Ask what would make it easier to do: due date, priority, owner, or one line of
754
- context.
776
+ 2. Ask whether it is an issue, one-session task, or subtask only when the level is
777
+ not already obvious.
778
+ 3. Ask where it belongs in the hierarchy: project for an issue, issue for a task, or
779
+ parent task for a subtask. Use goal or standalone only when the user is
780
+ intentionally outside the PM hierarchy.
781
+ 4. Capture the execution contract in `aiInstructions` when the work is meant for an
782
+ AI or agent session.
783
+ 5. Ask what would make it easier to do: due date, priority, owner, human/bot
784
+ assignees, acceptance criteria, or one line of context.
755
785
 
756
786
  Helpful follow-up lanes:
757
787
 
758
788
  - turn vague intent into an actionable verb
759
- - identify parent project or goal
760
- - capture the one timing or priority detail that will actually help
789
+ - decide whether the work item is an issue, task, or subtask
790
+ - identify parent project, issue, or task
791
+ - capture the one-session execution contract in `aiInstructions`
792
+ - decide whether one owner or several human/bot assignees need to be explicit
793
+ - capture the one timing, priority, or acceptance detail that will actually help
761
794
 
762
795
  Ready to save when:
763
796
 
764
797
  - the task is phrased as an actionable move
765
- - placement is clear enough
766
- - any crucial timing or priority is captured
798
+ - the level is clear enough: issue, task, or subtask
799
+ - placement is clear enough: project, issue, parent task, or intentional inbox
800
+ - any crucial timing, acceptance criteria, or execution instruction is captured
767
801
 
768
802
  Preferred opening question:
769
803
 
@@ -1760,7 +1794,8 @@ Direct action rules:
1760
1794
  "catalog" read when the user needs a runnable flow versus an input-box contract.
1761
1795
  - If the user wants to execute a known saved flow, use `/api/v1/workbench/flows/:id/run`.
1762
1796
  - If the user wants one-off input execution without depending on a saved flow id, use
1763
- `/api/v1/workbench/run`.
1797
+ `POST /api/v1/workbench/run` through the dedicated one-off execution lane and keep
1798
+ the user-facing question about the one-off input contract.
1764
1799
  - If the user wants to debug one failed execution, narrow whether they need the run
1765
1800
  detail, one node result, the latest node output, or the published output before you
1766
1801
  ask for flow changes.
@@ -339,6 +339,23 @@ Closing turn:
339
339
  linked
340
340
  - if the user says it lands, move toward the write instead of reopening exploration
341
341
 
342
+ ## Second-turn therapeutic discipline
343
+
344
+ After the user's first real answer, do not simply ask another broad origin question
345
+ and do not move into repair because the schema has optional fields left. Briefly name
346
+ the emotional center, function, danger, or value conflict that became clearer, then
347
+ choose one next lane: situation, sequence, meaning, protection, cost, longing,
348
+ naming, hypothesis, link, or save confirmation.
349
+
350
+ When one concrete example is already clear, a useful second or third turn can be a
351
+ careful interpretive hypothesis rather than another request for raw material. The
352
+ hypothesis should name what the response may be protecting, predicting, relieving, or
353
+ costing, then ask whether that lands or needs correction.
354
+
355
+ If the accepted formulation is already accurate enough to save, stop deepening. Ask
356
+ one accuracy question, preserve the user's wording, and write through the shared
357
+ batch entity route after consent.
358
+
342
359
  ## Name, Define, Connect
343
360
 
344
361
  Use this checkpoint once the material is coherent enough.